From a438cfe5cf6deeb265924cbf9af47590ab96e48f Mon Sep 17 00:00:00 2001 From: Ttibsi Date: Thu, 20 Aug 2026 10:37:05 +0000 Subject: [PATCH 1/5] Manage a file with tabs in --- src/controller.cpp | 14 +++++++------- src/model.cpp | 6 +++--- src/model.h | 3 ++- src/text_io.cpp | 28 +++++++++++++++++++++++----- src/text_io.h | 14 ++++++++++++-- 5 files changed, 47 insertions(+), 18 deletions(-) diff --git a/src/controller.cpp b/src/controller.cpp index 8683a65..65fe463 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -62,16 +62,16 @@ void Controller::create_view(const Flags& flags) { auto logger = spdlog::get("basic_logger"); if (logger != nullptr) { logger->info("Creating view from file: " + flags.file); } - opt_lines_t file_chars = open_file(flags.file); + ReadFile file_chars = open_file(flags.file); - if (file_chars.has_value()) { - models.emplace_back(file_chars.value(), flags.file); + if (file_chars.success) { + models.emplace_back(file_chars.lines, flags.file, file_chars.has_tabs); view.add_model(&models.at(models.size() - 1)); if (flags.lineno) { std::size_t line_num = std::min(flags.lineno, models.at(models.size() - 1).buf.size()); - view.cursor_down(uint32_t(std::min(line_num - 1, file_chars.value().size()))); + view.cursor_down(uint32_t(std::min(line_num - 1, file_chars.lines.size()))); view.center_current_line(); } } else { @@ -756,9 +756,9 @@ bool Controller::parse_command() { } void Controller::add_model(const std::string& filename) { - opt_lines_t contents = open_file(filename); - if (contents.has_value()) { - models.emplace_back(contents.value(), filename); + ReadFile contents = open_file(filename); + if (contents.success) { + models.emplace_back(contents.lines, filename, contents.has_tabs); } else { models.emplace_back(term_size.vertical - 2, filename); } diff --git a/src/model.cpp b/src/model.cpp index 4a77487..ae957ab 100644 --- a/src/model.cpp +++ b/src/model.cpp @@ -13,14 +13,14 @@ #include "text_io.h" Model::Model(const std::size_t view_height, std::string_view file_name) - : buf({""}), filename(file_name) { + : buf({""}), filename(file_name), tabbed_file(false) { buf.reserve(view_height); set_read_only(file_name); } // NOTE: Intentional copy of file_chars -Model::Model(std::vector file_chars, std::string_view file_name) - : buf(file_chars), filename(file_name) { +Model::Model(std::vector file_chars, std::string_view file_name, bool tabbed_file) + : buf(file_chars), filename(file_name), tabbed_file(tabbed_file) { set_read_only(file_name); } diff --git a/src/model.h b/src/model.h index 5f8fdff..0944191 100644 --- a/src/model.h +++ b/src/model.h @@ -44,6 +44,7 @@ struct Model { std::string filename; unsigned int current_line = 0; // 0-indexed unsigned int current_char = 0; // 0-indexed + bool tabbed_file = false; std::string search_str = ""; std::size_t vertical_offset = 0; std::unordered_map marks = {}; @@ -58,7 +59,7 @@ struct Model { std::stack redo_stack = {}; Model(std::size_t, std::string_view); - Model(std::vector, std::string_view); + Model(std::vector, std::string_view, bool); [[nodiscard]] Redraw backspace(); [[nodiscard]] std::size_t newline(); void insert(const char); diff --git a/src/text_io.cpp b/src/text_io.cpp index c9bcd2a..5b0329c 100644 --- a/src/text_io.cpp +++ b/src/text_io.cpp @@ -14,21 +14,23 @@ #include "spdlog/spdlog.h" #include "view.h" -[[nodiscard]] opt_lines_t open_file(const std::string& file) { - if (!get_file_size(file)) { return {}; } +[[nodiscard]] ReadFile open_file(const std::string& file) { + if (!get_file_size(file)) { return ReadFile(); } auto ret = std::vector(); std::string line = ""; char ch; std::ifstream ifs(file); - if (ifs.fail()) { return {}; } + if (ifs.fail()) { return ReadFile(); } + bool has_tabs = false; while (ifs.get(ch)) { switch (ch) { case '\r': break; case '\t': + has_tabs = true; line += std::string(TAB_SIZE, ' '); break; case '\n': @@ -44,7 +46,7 @@ // the vector if (line.size()) { ret.push_back(line); } - return ret; + return ReadFile(ret, has_tabs); } [[nodiscard]] unsigned int get_file_size(const std::string& file) { @@ -63,7 +65,8 @@ if (filename_input.has_value()) { model->filename = filename_input.value(); } std::ofstream out(model->filename); - for (auto&& line : model->buf) { + lines_t lines_to_write = convert_tabs(model->buf, model->tabbed_file); + for (auto&& line : lines_to_write) { rtrim(line); out << line << "\n"; } @@ -102,6 +105,7 @@ void rtrim(std::string& str) { } [[nodiscard]] std::string check_filename(const std::string& filename) { + return ""; std::string err_text = "Iris currently does not support tab-delineated files"; if (filename == "Makefile" || filename == "makefile") { @@ -208,3 +212,17 @@ void rtrim(std::string& str) { if (ret == std::string::npos) { return -1; } return int32_t(ret); } + +[[nodiscard]] lines_t convert_tabs(lines_t buf, const bool has_tabs) { + if (!has_tabs) { return buf; } + const std::string tab_chars = std::string(TAB_SIZE, ' '); + + for (auto&& line : buf) { + while (line.contains(tab_chars)) { + std::size_t replace_pos = line.find(tab_chars); + line = line.replace(replace_pos, TAB_SIZE, "\t"); + } + } + + return buf; +} diff --git a/src/text_io.h b/src/text_io.h index 686bacf..4e2433f 100644 --- a/src/text_io.h +++ b/src/text_io.h @@ -9,7 +9,16 @@ #include "model.h" using lines_t = std::vector; -using opt_lines_t = std::optional>; +// using opt_lines_t = std::optional>; + +struct ReadFile { + std::vector lines; + bool has_tabs; + bool success; + + ReadFile() : lines({}), has_tabs(false), success(false) {} + ReadFile(lines_t lines, bool has_tabs) : lines(lines), has_tabs(has_tabs), success(true) {} +}; struct WriteData { int bytes = 0; @@ -35,7 +44,7 @@ struct Response { std::string err = ""; }; -[[nodiscard]] opt_lines_t open_file(const std::string&); +[[nodiscard]] ReadFile open_file(const std::string&); [[nodiscard]] unsigned int get_file_size(const std::string&); [[nodiscard]] WriteData write_to_file(Model*, std::optional); void rtrim(std::string& str); @@ -46,5 +55,6 @@ void rtrim(std::string& str); [[nodiscard]] std::optional shell_exec(std::string); [[nodiscard]] std::vector split_by(const std::string&, const char); [[nodiscard]] int first_non_whitespace(const std::string&); +[[nodiscard]] lines_t convert_tabs(lines_t, const bool); #endif // TEXT_IO_H From 7f2d89b4aa29e533deff1a04eb0352b518b33afc Mon Sep 17 00:00:00 2001 From: Ttibsi Date: Thu, 20 Aug 2026 10:39:40 +0000 Subject: [PATCH 2/5] retire filename filtering --- src/main.cpp | 6 ------ src/text_io.cpp | 13 ------------- src/text_io.h | 1 - tests/text_io_test.cpp | 9 --------- 4 files changed, 29 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 109e414..cdc6608 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -66,12 +66,6 @@ int main(int argc, char* argv[]) { return 0; } - auto filename_error = check_filename(flags.file); - if (!(filename_error.empty())) { - std::println("{}", filename_error); - return 0; - } - try { auto logger = spdlog::basic_logger_mt("basic_logger", "iris.log"); } catch (const spdlog::spdlog_ex& ex) { std::println("Log init failed: {}", ex.what()); } diff --git a/src/text_io.cpp b/src/text_io.cpp index 5b0329c..255f933 100644 --- a/src/text_io.cpp +++ b/src/text_io.cpp @@ -104,19 +104,6 @@ void rtrim(std::string& str) { return std::find(alphabet.begin(), alphabet.end(), c) != alphabet.end(); } -[[nodiscard]] std::string check_filename(const std::string& filename) { - return ""; - std::string err_text = "Iris currently does not support tab-delineated files"; - - if (filename == "Makefile" || filename == "makefile") { - return err_text; - } else if (filename.ends_with(".go")) { - return err_text; - } - - return ""; -} - // https://stackoverflow.com/a/12774387 [[nodiscard]] bool file_exists(std::string_view name) { struct stat buffer; diff --git a/src/text_io.h b/src/text_io.h index 4e2433f..69bf5bb 100644 --- a/src/text_io.h +++ b/src/text_io.h @@ -50,7 +50,6 @@ struct Response { void rtrim(std::string& str); [[nodiscard]] lines_t lines(const std::string&); [[nodiscard]] bool is_letter(const char&); -[[nodiscard]] std::string check_filename(const std::string&); [[nodiscard]] bool file_exists(std::string_view); [[nodiscard]] std::optional shell_exec(std::string); [[nodiscard]] std::vector split_by(const std::string&, const char); diff --git a/tests/text_io_test.cpp b/tests/text_io_test.cpp index a4e1505..d09a144 100644 --- a/tests/text_io_test.cpp +++ b/tests/text_io_test.cpp @@ -83,15 +83,6 @@ TEST_CASE("is_letter", "[textio]") { REQUIRE_FALSE(is_letter(':')); } -TEST_CASE("check_filename", "[textio]") { - std::string err_text = "Iris currently does not support tab-delineated files"; - - REQUIRE(check_filename("Makefile") == err_text); - REQUIRE(check_filename("makefile") == err_text); - REQUIRE(check_filename("t.go") == err_text); - REQUIRE(check_filename("t.py") == ""); -} - TEST_CASE("file_exists", "[textio]") { REQUIRE(file_exists("tests/text_io_test.cpp")); REQUIRE(!(file_exists("tests/something_else.cpp"))); From 93d4846844593c569b91b6800588e80fd4fca61a Mon Sep 17 00:00:00 2001 From: Ttibsi Date: Thu, 20 Aug 2026 11:22:10 +0000 Subject: [PATCH 3/5] existing unit tests refactored --- tests/model_test.cpp | 78 ++++++++++++++++----------------- tests/text_io_test.cpp | 20 +++++---- tests/view_test.cpp | 98 ++++++++++++++++++++++++------------------ 3 files changed, 107 insertions(+), 89 deletions(-) diff --git a/tests/model_test.cpp b/tests/model_test.cpp index 5bab5f5..3937164 100644 --- a/tests/model_test.cpp +++ b/tests/model_test.cpp @@ -20,7 +20,7 @@ TEST_CASE("Constructor", "[model]") { TEST_CASE("Constructor with params", "[model]") { lines_t expected_buf = {"foo", "bar", "baz"}; - auto m = Model(expected_buf, ""); + auto m = Model(expected_buf, "", false); REQUIRE(m.filename == ""); REQUIRE(m.buf.at(0) == "foo"); @@ -29,7 +29,7 @@ TEST_CASE("Constructor with params", "[model]") { TEST_CASE("backspace", "[model]") { SECTION("backspace a newline") { lines_t v = {"foo", "bar", "baz"}; - auto m = Model(v, ""); + auto m = Model(v, "", false); m.current_line = 1; std::ignore = m.backspace(); @@ -39,7 +39,7 @@ TEST_CASE("backspace", "[model]") { SECTION("backspace a char") { lines_t v = {"foo", "bar", "baz"}; - auto m = Model(v, ""); + auto m = Model(v, "", false); m.current_line = 1; m.current_char = 2; std::ignore = m.backspace(); @@ -51,7 +51,7 @@ TEST_CASE("backspace", "[model]") { SECTION("backspace the last char") { lines_t v = {"foo", "bar", "baz"}; - auto m = Model(v, ""); + auto m = Model(v, "", false); m.current_line = 1; m.current_char = 3; std::ignore = m.backspace(); @@ -63,7 +63,7 @@ TEST_CASE("backspace", "[model]") { SECTION("backspace a tab-space") { lines_t v = {" Some tab"}; - auto m = Model(v, ""); + auto m = Model(v, "", false); m.current_char = 4; Redraw draw = m.backspace(); @@ -78,7 +78,7 @@ TEST_CASE("backspace", "[model]") { TEST_CASE("newline", "[model]") { SECTION("At end of line") { lines_t v = {"foo", "bar", "baz"}; - auto m = Model(v, ""); + auto m = Model(v, "", false); m.current_char = 3; const std::size_t prev_line_len = m.newline(); @@ -91,7 +91,7 @@ TEST_CASE("newline", "[model]") { SECTION("At mid of line") { lines_t v = {"foo", "bar", "baz"}; - auto m = Model(v, ""); + auto m = Model(v, "", false); m.current_char = 1; const std::size_t prev_line_len = m.newline(); @@ -106,7 +106,7 @@ TEST_CASE("newline", "[model]") { SECTION("At start of line") { lines_t v = {"foo", "bar", "baz"}; - auto m = Model(v, ""); + auto m = Model(v, "", false); const std::size_t prev_line_len = m.newline(); @@ -120,7 +120,7 @@ TEST_CASE("newline", "[model]") { SECTION("Remove whitespace from second line") { lines_t v = {"Some long text", "and another"}; - auto m = Model(v, ""); + auto m = Model(v, "", false); m.current_char = 4; const std::size_t prev_line_len = m.newline(); @@ -132,7 +132,7 @@ TEST_CASE("newline", "[model]") { SECTION("Insert indentation") { lines_t v = {" an indented line"}; - auto m = Model(v, ""); + auto m = Model(v, "", false); m.current_char = 15; const std::size_t line_one_len = m.newline(); @@ -145,7 +145,7 @@ TEST_CASE("newline", "[model]") { TEST_CASE("insert", "[model]") { SECTION("Insert at start of line") { lines_t v = {"foo", "bar", "baz"}; - auto m = Model(v, ""); + auto m = Model(v, "", false); m.insert('x'); REQUIRE(m.buf.at(0) == "xfoo"); @@ -160,7 +160,7 @@ TEST_CASE("insert", "[model]") { SECTION("Insert in mid of line") { lines_t v = {"foo", "bar", "baz"}; - auto m = Model(v, ""); + auto m = Model(v, "", false); m.current_char++; m.insert('x'); @@ -172,7 +172,7 @@ TEST_CASE("insert", "[model]") { SECTION("Insert at end of line") { lines_t v = {"foo", "bar", "baz"}; - auto m = Model(v, ""); + auto m = Model(v, "", false); m.current_char = static_cast(v.at(0).size()); m.insert('x'); @@ -185,13 +185,13 @@ TEST_CASE("insert", "[model]") { TEST_CASE("lineno_in_scope", "[model]") { lines_t v = {"foo", "bar", "baz"}; - auto m = Model(v, ""); + auto m = Model(v, "", false); REQUIRE(m.lineno_in_scope(2)); REQUIRE_FALSE(m.lineno_in_scope(6)); } TEST_CASE("next_word_pos", "[model]") { - auto m = Model({"This is the first line", "std::foo();"}, ""); + auto m = Model({"This is the first line", "std::foo();"}, "", false); REQUIRE(m.next_word_pos().has_value()); REQUIRE(m.next_word_pos().value() == 5); @@ -211,7 +211,7 @@ TEST_CASE("next_word_pos", "[model]") { } TEST_CASE("prev_word_pos", "[model]") { - auto m = Model({"This is the first line", "std::foo();"}, ""); + auto m = Model({"This is the first line", "std::foo();"}, "", false); m.current_char = 21; REQUIRE(m.prev_word_pos().has_value()); @@ -231,7 +231,7 @@ TEST_CASE("prev_word_pos", "[model]") { } TEST_CASE("next_para_pos", "[model]") { - auto m = Model({"line one", "line two", "line three", "", "line four", "line five"}, ""); + auto m = Model({"line one", "line two", "line three", "", "line four", "line five"}, "", false); auto opt = m.next_para_pos(); REQUIRE(opt.has_value()); @@ -249,7 +249,7 @@ TEST_CASE("next_para_pos", "[model]") { } TEST_CASE("prev_para_pos", "[model]") { - auto m = Model({"line one", "line two", "line three", "", "line four", "line five"}, ""); + auto m = Model({"line one", "line two", "line three", "", "line four", "line five"}, "", false); auto opt = m.prev_para_pos(); REQUIRE_FALSE(opt.has_value()); @@ -267,7 +267,7 @@ TEST_CASE("prev_para_pos", "[model]") { } TEST_CASE("end_of_word_pos", "[model]") { - auto m = Model({"line one", "", "line five"}, ""); + auto m = Model({"line one", "", "line five"}, "", false); m.current_char = 7; REQUIRE(m.end_of_word_pos() == std::nullopt); @@ -286,7 +286,7 @@ TEST_CASE("end_of_word_pos", "[model]") { } TEST_CASE("replace_char", "[model]") { - auto m = Model({"line one", "", "line five"}, ""); + auto m = Model({"line one", "", "line five"}, "", false); m.current_line = 1; m.replace_char('c'); @@ -302,7 +302,7 @@ TEST_CASE("replace_char", "[model]") { } TEST_CASE("toggle_case", "[model]") { - auto m = Model({"line one", "line two", "line three", "", "line four", "line five"}, ""); + auto m = Model({"line one", "line two", "line three", "", "line four", "line five"}, "", false); m.toggle_case(); REQUIRE(m.buf.at(0).at(0) == 'L'); @@ -312,7 +312,7 @@ TEST_CASE("toggle_case", "[model]") { } TEST_CASE("find_next", "[model]") { - auto m = Model({"line one", "line two", "line three", "", "line four", "line five"}, ""); + auto m = Model({"line one", "line two", "line three", "", "line four", "line five"}, "", false); auto ret = m.find_next('o'); REQUIRE(ret.has_value()); @@ -335,7 +335,7 @@ TEST_CASE("find_next", "[model]") { } TEST_CASE("find_prev", "[model]") { - auto m = Model({"line one", "line two", "line three", "", "line four", "line five"}, ""); + auto m = Model({"line one", "line two", "line three", "", "line four", "line five"}, "", false); m.current_line = 5; m.current_char = 8; @@ -363,7 +363,7 @@ TEST_CASE("find_prev", "[model]") { } TEST_CASE("undo", "[model]") { - auto m = Model({"line one", "line two", "line three", "", "line four", "line five"}, ""); + auto m = Model({"line one", "line two", "line three", "", "line four", "line five"}, "", false); // NOTE: In the actual execution, wemove the cursor back one then perform // the same change as DelCurrentChar -- note that the current_char is one @@ -509,7 +509,7 @@ TEST_CASE("undo", "[model]") { } TEST_CASE("get_current_char", "[model]") { - auto m = Model({"line one", "line two", "line three", "", "line four", "line five"}, ""); + auto m = Model({"line one", "line two", "line three", "", "line four", "line five"}, "", false); REQUIRE(m.get_current_char() == 'l'); m.current_line = 2; @@ -518,7 +518,7 @@ TEST_CASE("get_current_char", "[model]") { } TEST_CASE("redo", "[model]") { - auto m = Model({"line one", "line two", "line three", "", "line four", "line five"}, ""); + auto m = Model({"line one", "line two", "line three", "", "line four", "line five"}, "", false); m.current_line = 1; m.current_char = 1; @@ -657,7 +657,7 @@ TEST_CASE("redo", "[model]") { } TEST_CASE("move_line_down", "[model]") { - auto m = Model({"line one", "line two", "line three", "", "line four", "line five"}, ""); + auto m = Model({"line one", "line two", "line three", "", "line four", "line five"}, "", false); REQUIRE(m.move_line_down()); REQUIRE(m.buf.at(0) == "line two"); @@ -671,7 +671,7 @@ TEST_CASE("move_line_down", "[model]") { } TEST_CASE("move_line_up", "[model]") { - auto m = Model({"line one", "line two", "line three", "", "line four", "line five"}, ""); + auto m = Model({"line one", "line two", "line three", "", "line four", "line five"}, "", false); m.current_line = 5; REQUIRE(m.move_line_up()); @@ -696,14 +696,14 @@ TEST_CASE("set_read_only", "[model]") { } TEST_CASE("delete_current_line", "[model]") { - auto m = Model({"line one", "line two", "line three", "", "line four", "line five"}, ""); + auto m = Model({"line one", "line two", "line three", "", "line four", "line five"}, "", false); m.delete_current_line(); REQUIRE(m.buf.size() == 5); REQUIRE(m.buf.at(0) == "line two"); } TEST_CASE("current_word", "[model]") { - auto m = Model({"line one", "line two", "line three", "", "line four", "line five"}, ""); + auto m = Model({"line one", "line two", "line three", "", "line four", "line five"}, "", false); SECTION("Middle of word") { m.current_line = 1; @@ -728,7 +728,7 @@ TEST_CASE("current_word", "[model]") { } TEST_CASE("delete_current_word", "[model]") { - auto m = Model({"line one", "line two", "line three", "", "line four", "line five"}, ""); + auto m = Model({"line one", "line two", "line three", "", "line four", "line five"}, "", false); m.current_line = 1; m.current_char = 2; @@ -746,7 +746,7 @@ TEST_CASE("search_text", "[model]") { auto m = Model( {"line one", "line two", "line three", "", "line four", "line five", "line six", "line seven", "line eight", "line nine", "line ten"}, - ""); + "", false); std::vector ret = m.search_text("one"); REQUIRE(ret.size() == 1); REQUIRE(rawterm::raw_str(ret.at(0)) == "|1| line one"); @@ -763,7 +763,7 @@ TEST_CASE("search_and_replace", "[model]") { auto m = Model( {"line one", "line two", "line three", "", "line four", "line five", "line six", "line seven", "line eight", "line nine", "line ten"}, - ""); + "", false); m.search_and_replace("one|TEST"); REQUIRE(m.buf.at(0) == "line TEST"); @@ -779,7 +779,7 @@ TEST_CASE("find_next_str", "[model]") { auto m = Model( {"line one", "line two", "line three", "", "line four", "line five", "line six", "line seven", "line eight", "line nine", "line ten"}, - ""); + "", false); SECTION("Text not found") { REQUIRE(m.find_next_str(";f unknown") == std::nullopt); @@ -801,7 +801,7 @@ TEST_CASE("find_next_str", "[model]") { } TEST_CASE("indent_curr_line", "[model]") { - auto m = Model({"foo", "bar"}, ""); + auto m = Model({"foo", "bar"}, "", false); m.indent_curr_line(); REQUIRE(m.buf.at(0).size() == 7); @@ -812,7 +812,7 @@ TEST_CASE("indent_curr_line", "[model]") { } TEST_CASE("dedent_curr_line", "[model]") { - auto m = Model({" foo", "bar"}, ""); + auto m = Model({" foo", "bar"}, "", false); // Dedent a line that has some indentation m.dedent_curr_line(); @@ -828,7 +828,7 @@ TEST_CASE("dedent_curr_line", "[model]") { } TEST_CASE("add_mark", "[model]") { - auto m = Model({" foo", "bar"}, ""); + auto m = Model({" foo", "bar"}, "", false); m.add_mark('a'); REQUIRE(m.marks.size() == 1); @@ -837,7 +837,7 @@ TEST_CASE("add_mark", "[model]") { } TEST_CASE("is_marked", "[model]") { - auto m = Model({" foo", "bar"}, ""); + auto m = Model({" foo", "bar"}, "", false); m.add_mark('a'); REQUIRE(m.is_marked(0)); @@ -845,7 +845,7 @@ TEST_CASE("is_marked", "[model]") { } TEST_CASE("go_to_mark", "[model]") { - auto m = Model({" foo", "bar"}, ""); + auto m = Model({" foo", "bar"}, "", false); m.current_line = 1; m.current_char = 1; m.add_mark('a'); diff --git a/tests/text_io_test.cpp b/tests/text_io_test.cpp index d09a144..bda7f8d 100644 --- a/tests/text_io_test.cpp +++ b/tests/text_io_test.cpp @@ -9,18 +9,20 @@ TEST_CASE("open_file", "[textio]") { lines_t expected = { "This is some text", " here is a newline and a tab", "and another newline"}; - opt_lines_t actual = open_file("tests/fixture/test_file_1.txt"); + const ReadFile actual = open_file("tests/fixture/test_file_1.txt"); - REQUIRE(actual.has_value() == true); - REQUIRE(actual.value() == expected); - REQUIRE(actual.value().at(1) == expected.at(1)); + REQUIRE(actual.success == true); + REQUIRE(actual.lines == expected); + REQUIRE(actual.lines.at(1) == expected.at(1)); + REQUIRE(actual.has_tabs == false); } SECTION("One line, no newlines") { - opt_lines_t actual = open_file("tests/fixture/no_newline_file.txt"); - REQUIRE(actual.has_value() == true); - REQUIRE(actual.value().size() == 1); - REQUIRE(actual.value().at(0) == "hello"); + const ReadFile actual = open_file("tests/fixture/no_newline_file.txt"); + REQUIRE(actual.success == true); + REQUIRE(actual.lines.size() == 1); + REQUIRE(actual.lines.at(0) == "hello"); + REQUIRE(actual.has_tabs == false); } } @@ -31,7 +33,7 @@ TEST_CASE("get_file_size", "[textio]") { TEST_CASE("write_to_file", "[textio]") { lines_t expected_buf = {"foo", "bar", "baz"}; - auto m = Model(expected_buf, "tests/fixture/temp_file.txt"); + auto m = Model(expected_buf, "tests/fixture/temp_file.txt", false); SECTION("Filename already set") { const WriteData data = write_to_file(&m, std::nullopt); diff --git a/tests/view_test.cpp b/tests/view_test.cpp index a21c02d..c484977 100644 --- a/tests/view_test.cpp +++ b/tests/view_test.cpp @@ -22,7 +22,7 @@ TEST_CASE("add_model", "[view]") { lines_t raw = {"This is some text", " here is a newline and tab", "and another newline"}; auto v = View(&c, rawterm::Pos(24, 80)); - auto m = Model(raw, "test_file.txt"); + auto m = Model(raw, "test_file.txt", false); v.add_model(&m); REQUIRE(v.active_model == 0); @@ -36,7 +36,7 @@ TEST_CASE("get_active_model", "[view]") { lines_t raw = {"This is some text", " here is a newline and tab", "and another newline"}; auto v = View(&c, rawterm::Pos(24, 80)); - auto m = Model(raw, "test_file.txt"); + auto m = Model(raw, "test_file.txt", false); v.add_model(&m); REQUIRE(v.get_active_model() == &m); @@ -47,7 +47,8 @@ TEST_CASE("render_screen", "[view]") { Controller c; auto v = View(&c, rawterm::Pos(24, 80)); auto m = Model( - open_file("tests/fixture/test_file_1.txt").value(), "tests/fixture/test_file_1.txt"); + open_file("tests/fixture/test_file_1.txt").lines, "tests/fixture/test_file_1.txt", + false); v.add_model(&m); auto buffer = lines(v.render_screen()); @@ -80,11 +81,11 @@ TEST_CASE("render_tab_bar", "[view]") { lines_t raw = {"This is some text", " here is a newline and tab", "and another newline"}; auto v = View(&c, rawterm::Pos(24, 80)); - auto m = Model(raw, "test_file.txt"); + auto m = Model(raw, "test_file.txt", false); v.add_model(&m); - auto m2 = - Model(open_file("tests/fixture/test_file_1.txt").value(), "tests/fixture/test_file_1.txt"); + auto m2 = Model( + open_file("tests/fixture/test_file_1.txt").lines, "tests/fixture/test_file_1.txt", false); v.add_model(&m2); SECTION("Unmodified") { @@ -111,7 +112,8 @@ TEST_CASE("render_line", "[view]") { SECTION("Standard line rendering") { Controller c; auto m = Model( - open_file("tests/fixture/test_file_1.txt").value(), "tests/fixture/test_file_1.txt"); + open_file("tests/fixture/test_file_1.txt").lines, "tests/fixture/test_file_1.txt", + false); auto v = View(&c, rawterm::Pos(24, 80)); v.add_model(&m); @@ -152,8 +154,8 @@ TEST_CASE("render_status_bar", "[view]") { Controller c; auto v = View(&c, rawterm::Pos(24, 100)); - auto m = - Model(open_file("tests/fixture/test_file_1.txt").value(), "tests/fixture/test_file_1.txt"); + auto m = Model( + open_file("tests/fixture/test_file_1.txt").lines, "tests/fixture/test_file_1.txt", false); v.add_model(&m); std::string ret = v.render_status_bar(); @@ -168,8 +170,8 @@ TEST_CASE("clamp_horizontal_movement", "[view]") { Controller c; auto v = View(&c, rawterm::Pos(24, 80)); - auto m = - Model(open_file("tests/fixture/lorem_ipsum.txt").value(), "tests/fixture/lorem_ipsum.txt"); + auto m = Model( + open_file("tests/fixture/lorem_ipsum.txt").lines, "tests/fixture/lorem_ipsum.txt", false); v.add_model(&m); m.current_line = 7; @@ -188,7 +190,8 @@ TEST_CASE("cursor_left", "[view]") { Controller c; auto v = View(&c, rawterm::Pos(24, 80)); auto m = Model( - open_file("tests/fixture/test_file_1.txt").value(), "tests/fixture/test_file_1.txt"); + open_file("tests/fixture/test_file_1.txt").lines, "tests/fixture/test_file_1.txt", + false); v.add_model(&m); REQUIRE(v.cur == rawterm::Pos(1, 1)); @@ -200,7 +203,8 @@ TEST_CASE("cursor_left", "[view]") { Controller c; auto v = View(&c, rawterm::Pos(24, 80)); auto m = Model( - open_file("tests/fixture/test_file_1.txt").value(), "tests/fixture/test_file_1.txt"); + open_file("tests/fixture/test_file_1.txt").lines, "tests/fixture/test_file_1.txt", + false); v.add_model(&m); v.cursor_right(4); @@ -216,7 +220,8 @@ TEST_CASE("cursor_up", "[view]") { Controller c; auto v = View(&c, rawterm::Pos(24, 80)); auto m = Model( - open_file("tests/fixture/test_file_1.txt").value(), "tests/fixture/test_file_1.txt"); + open_file("tests/fixture/test_file_1.txt").lines, "tests/fixture/test_file_1.txt", + false); v.add_model(&m); REQUIRE(v.cur == rawterm::Pos(1, 1)); @@ -228,7 +233,8 @@ TEST_CASE("cursor_up", "[view]") { Controller c; auto v = View(&c, rawterm::Pos(24, 80)); auto m = Model( - open_file("tests/fixture/test_file_1.txt").value(), "tests/fixture/test_file_1.txt"); + open_file("tests/fixture/test_file_1.txt").lines, "tests/fixture/test_file_1.txt", + false); v.add_model(&m); v.cursor_down(); v.cursor_down(); @@ -244,7 +250,8 @@ TEST_CASE("cursor_up", "[view]") { Controller c; auto v = View(&c, rawterm::Pos(24, 80)); auto m = Model( - open_file("tests/fixture/lorem_ipsum.txt").value(), "tests/fixture/lorem_ipsum.txt"); + open_file("tests/fixture/lorem_ipsum.txt").lines, "tests/fixture/lorem_ipsum.txt", + false); v.add_model(&m); // Scroll down below initial view @@ -274,7 +281,8 @@ TEST_CASE("cursor_down", "[view]") { SECTION("Move cursor down") { Controller c; auto m = Model( - open_file("tests/fixture/lorem_ipsum.txt").value(), "tests/fixture/lorem_ipsum.txt"); + open_file("tests/fixture/lorem_ipsum.txt").lines, "tests/fixture/lorem_ipsum.txt", + false); auto v = View(&c, rawterm::Pos(24, 80)); v.add_model(&m); @@ -287,7 +295,8 @@ TEST_CASE("cursor_down", "[view]") { SECTION("Move view down (scroll)") { Controller c; auto m = Model( - open_file("tests/fixture/lorem_ipsum.txt").value(), "tests/fixture/lorem_ipsum.txt"); + open_file("tests/fixture/lorem_ipsum.txt").lines, "tests/fixture/lorem_ipsum.txt", + false); auto v = View(&c, rawterm::Pos(24, 80)); v.add_model(&m); @@ -311,7 +320,8 @@ TEST_CASE("cursor_down", "[view]") { SECTION("Already at bottom-most row in file") { Controller c; auto m = Model( - open_file("tests/fixture/lorem_ipsum.txt").value(), "tests/fixture/lorem_ipsum.txt"); + open_file("tests/fixture/lorem_ipsum.txt").lines, "tests/fixture/lorem_ipsum.txt", + false); auto v = View(&c, rawterm::Pos(24, 80)); v.add_model(&m); @@ -330,7 +340,8 @@ TEST_CASE("cursor_down", "[view]") { SECTION("bottom row of file within view (no scrolling required)") { Controller c; auto m = Model( - open_file("tests/fixture/test_file_1.txt").value(), "tests/fixture/test_file_1.txt"); + open_file("tests/fixture/test_file_1.txt").lines, "tests/fixture/test_file_1.txt", + false); auto v = View(&c, rawterm::Pos(24, 80)); v.add_model(&m); @@ -352,7 +363,8 @@ TEST_CASE("cursor_right", "[view]") { Controller c; auto v = View(&c, rawterm::Pos(24, 80)); auto m = Model( - open_file("tests/fixture/test_file_1.txt").value(), "tests/fixture/test_file_1.txt"); + open_file("tests/fixture/test_file_1.txt").lines, "tests/fixture/test_file_1.txt", + false); v.add_model(&m); v.cur.move({v.cur.vertical, int(v.line_number_offset + 1)}); @@ -366,7 +378,8 @@ TEST_CASE("cursor_right", "[view]") { Controller c; auto v = View(&c, rawterm::Pos(24, 80)); auto m = Model( - open_file("tests/fixture/test_file_1.txt").value(), "tests/fixture/test_file_1.txt"); + open_file("tests/fixture/test_file_1.txt").lines, "tests/fixture/test_file_1.txt", + false); v.add_model(&m); v.cur.move({v.cur.vertical, int(v.line_number_offset + 1)}); @@ -379,7 +392,8 @@ TEST_CASE("cursor_right", "[view]") { Controller c; auto v = View(&c, rawterm::Pos(24, 80)); auto m = Model( - open_file("tests/fixture/test_file_1.txt").value(), "tests/fixture/test_file_1.txt"); + open_file("tests/fixture/test_file_1.txt").lines, "tests/fixture/test_file_1.txt", + false); v.add_model(&m); v.cursor_right(1); @@ -390,8 +404,8 @@ TEST_CASE("cursor_right", "[view]") { TEST_CASE("cursor_end_of_line", "[view]") { Controller c; auto v = View(&c, rawterm::Pos(24, 80)); - auto m = - Model(open_file("tests/fixture/test_file_1.txt").value(), "tests/fixture/test_file_1.txt"); + auto m = Model( + open_file("tests/fixture/test_file_1.txt").lines, "tests/fixture/test_file_1.txt", false); v.add_model(&m); REQUIRE(v.cur == rawterm::Pos(1, 1)); @@ -404,7 +418,8 @@ TEST_CASE("center_current_line", "[view]") { Controller c; auto v = View(&c, rawterm::Pos(24, 80)); auto m = Model( - open_file("tests/fixture/lorem_ipsum.txt").value(), "tests/fixture/lorem_ipsum.txt"); + open_file("tests/fixture/lorem_ipsum.txt").lines, "tests/fixture/lorem_ipsum.txt", + false); v.add_model(&m); // Initial state @@ -424,7 +439,8 @@ TEST_CASE("center_current_line", "[view]") { Controller c; auto v = View(&c, rawterm::Pos(24, 80)); auto m = Model( - open_file("tests/fixture/lorem_ipsum.txt").value(), "tests/fixture/lorem_ipsum.txt"); + open_file("tests/fixture/lorem_ipsum.txt").lines, "tests/fixture/lorem_ipsum.txt", + false); v.add_model(&m); // Move cursor far down @@ -451,8 +467,8 @@ TEST_CASE("center_current_line", "[view]") { TEST_CASE("set_current_line", "[view]") { Controller c; auto v = View(&c, rawterm::Pos(24, 80)); - auto m = - Model(open_file("tests/fixture/lorem_ipsum.txt").value(), "tests/fixture/lorem_ipsum.txt"); + auto m = Model( + open_file("tests/fixture/lorem_ipsum.txt").lines, "tests/fixture/lorem_ipsum.txt", false); v.add_model(&m); SECTION("Move cursor to line already on screen") { @@ -483,8 +499,8 @@ TEST_CASE("set_current_line", "[view]") { TEST_CASE("tab_new", "[view]") { Controller c; auto v = View(&c, rawterm::Pos(24, 80)); - auto m = - Model(open_file("tests/fixture/lorem_ipsum.txt").value(), "tests/fixture/lorem_ipsum.txt"); + auto m = Model( + open_file("tests/fixture/lorem_ipsum.txt").lines, "tests/fixture/lorem_ipsum.txt", false); v.add_model(&m); v.tab_new(); @@ -500,8 +516,8 @@ TEST_CASE("tab_new", "[view]") { TEST_CASE("tab_next", "[view]") { Controller c; auto v = View(&c, rawterm::Pos(24, 80)); - auto m = - Model(open_file("tests/fixture/lorem_ipsum.txt").value(), "tests/fixture/lorem_ipsum.txt"); + auto m = Model( + open_file("tests/fixture/lorem_ipsum.txt").lines, "tests/fixture/lorem_ipsum.txt", false); v.add_model(&m); v.tab_new(); @@ -517,8 +533,8 @@ TEST_CASE("tab_next", "[view]") { TEST_CASE("tab_prev", "[view]") { Controller c; auto v = View(&c, rawterm::Pos(24, 80)); - auto m = - Model(open_file("tests/fixture/lorem_ipsum.txt").value(), "tests/fixture/lorem_ipsum.txt"); + auto m = Model( + open_file("tests/fixture/lorem_ipsum.txt").lines, "tests/fixture/lorem_ipsum.txt", false); v.add_model(&m); v.tab_new(); @@ -534,8 +550,8 @@ TEST_CASE("tab_prev", "[view]") { TEST_CASE("visible_tab_bar", "[view]") { Controller c; auto v = View(&c, rawterm::Pos(24, 80)); - auto m = - Model(open_file("tests/fixture/lorem_ipsum.txt").value(), "tests/fixture/lorem_ipsum.txt"); + auto m = Model( + open_file("tests/fixture/lorem_ipsum.txt").lines, "tests/fixture/lorem_ipsum.txt", false); v.add_model(&m); REQUIRE(v.visible_tab_bar() == 0); @@ -548,8 +564,8 @@ TEST_CASE("visible_tab_bar", "[view]") { TEST_CASE("set_lineno_offset", "[view]") { Controller c; auto v = View(&c, rawterm::Pos(24, 80)); - auto m = - Model(open_file("tests/fixture/lorem_ipsum.txt").value(), "tests/fixture/lorem_ipsum.txt"); + auto m = Model( + open_file("tests/fixture/lorem_ipsum.txt").lines, "tests/fixture/lorem_ipsum.txt", false); v.add_model(&m); unsigned int ret = v.set_lineno_offset(&m); @@ -564,8 +580,8 @@ TEST_CASE("set_lineno_offset", "[view]") { TEST_CASE("change_model_cursor", "[view]") { Controller c; auto v = View(&c, rawterm::Pos(24, 80)); - auto m = - Model(open_file("tests/fixture/lorem_ipsum.txt").value(), "tests/fixture/lorem_ipsum.txt"); + auto m = Model( + open_file("tests/fixture/lorem_ipsum.txt").lines, "tests/fixture/lorem_ipsum.txt", false); v.add_model(&m); v.cursor_right(5); From fa55f165b2d7a5e9037b829692b20a4b4929d037 Mon Sep 17 00:00:00 2001 From: Ttibsi Date: Thu, 20 Aug 2026 18:35:15 +0000 Subject: [PATCH 4/5] convert_tabs unit test --- tests/text_io_test.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/text_io_test.cpp b/tests/text_io_test.cpp index bda7f8d..1e63fd9 100644 --- a/tests/text_io_test.cpp +++ b/tests/text_io_test.cpp @@ -117,3 +117,22 @@ TEST_CASE("first_non_whitespace", "[textio]") { REQUIRE(first_non_whitespace(" test") == 4); REQUIRE(first_non_whitespace("test") == 0); } + +TEST_CASE("convert_tabs", "[textio]") { + SECTION("No tabs present") { + const std::vector expected = {"foo", " bar"}; + REQUIRE(expected == convert_tabs(expected, false)); + } + + SECTION("One tab present") { + const std::vector in = {"foo", " bar"}; + const std::vector expected = {"foo", "\tbar"}; + REQUIRE(expected == convert_tabs(in, true)); + } + + SECTION("Multiple tabs present") { + const std::vector in = {"foo", " bar"}; + const std::vector expected = {"foo", "\t\tbar"}; + REQUIRE(expected == convert_tabs(in, true)); + } +} From b2432b7aee1d4924d291236bc7aaa471ee595fb1 Mon Sep 17 00:00:00 2001 From: Ttibsi Date: Fri, 21 Aug 2026 21:20:37 +0000 Subject: [PATCH 5/5] tabbed file integration test --- tests/fixture/tabbed_file.txt | 2 ++ tests/integration/ui_test.py | 9 +++++++++ 2 files changed, 11 insertions(+) create mode 100644 tests/fixture/tabbed_file.txt diff --git a/tests/fixture/tabbed_file.txt b/tests/fixture/tabbed_file.txt new file mode 100644 index 0000000..08ff92b --- /dev/null +++ b/tests/fixture/tabbed_file.txt @@ -0,0 +1,2 @@ +foo + barb diff --git a/tests/integration/ui_test.py b/tests/integration/ui_test.py index 3536344..9280e4d 100644 --- a/tests/integration/ui_test.py +++ b/tests/integration/ui_test.py @@ -401,3 +401,12 @@ def test_readonly_cli_flag(): with TmuxRunner("bash", "--norc") as r: r.press_and_enter("./build/src/iris -r tests/fixture/test_file_1.txt") assert "[RO]" in r.await_statusbar_parts() + + +def test_tabbed_file(): + with TmuxRunner("bash", "--norc") as r: + r.press_and_enter("./build/src/iris -r tests/fixture/tabbed_file.txt") + assert r.await_statusbar_parts() + lines: list[str] = r.lines() + assert "foo" in lines[0] + assert " bar" in lines[1]