From 67ad3d51187f8e3a0229a0ba4e27d6c826d4853a Mon Sep 17 00:00:00 2001 From: Ttibsi Date: Mon, 23 Mar 2026 09:33:09 +0000 Subject: [PATCH 1/7] dedent line with left chevron --- src/action.h | 7 +++++++ src/controller.cpp | 6 ++++++ src/model.cpp | 9 +++++++++ src/model.h | 1 + 4 files changed, 23 insertions(+) diff --git a/src/action.h b/src/action.h index 2bed2d2..696e13e 100644 --- a/src/action.h +++ b/src/action.h @@ -15,6 +15,7 @@ enum class ActionType { // Pass no values Backspace, CenterCurrentLine, + DedentLine, DelCurrentChar, DelCurrentLine, DelCurrentWord, @@ -109,6 +110,12 @@ template v->center_current_line(); } break; + case ActionType::DedentLine: { + auto logger = spdlog::get("basic_logger"); + if (logger != nullptr) { logger->info("Action called: DedentLine"); } + v->get_active_model()->dedent_curr_line(); + } break; + case ActionType::DelCurrentChar: { if constexpr (std::is_same_v) { auto logger = spdlog::get("basic_logger"); diff --git a/src/controller.cpp b/src/controller.cpp index 8b78bb0..385a211 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -399,6 +399,12 @@ void Controller::start_action_engine() { parse_action(&view, Action {ActionType::ToggleCase}); view.draw_line(Draw_Line_dir::None); + + // Dedent line + } else if (k.value() == rawterm::Key('<')) { + if (is_readonly_model()) { continue; } + parse_action(&view, Action {ActionType::DedentLine}); + view.draw_line(Draw_Line_dir::None); } } diff --git a/src/model.cpp b/src/model.cpp index 55c960d..aa7d76b 100644 --- a/src/model.cpp +++ b/src/model.cpp @@ -528,3 +528,12 @@ std::optional Model::find_next_str(std::string_view sv) { return std::nullopt; } + +void Model::dedent_curr_line() { + auto offset = buf.at(current_line).find_first_not_of(' '); + if (offset == std::string::npos) { return; } + + const std::size_t to_delete = std::min(4ul, offset); + const std::size_t line_size = buf.at(current_line).size(); + buf.at(current_line) = buf.at(current_line).substr(to_delete, line_size); +} diff --git a/src/model.h b/src/model.h index d07f2f8..6fb5b8a 100644 --- a/src/model.h +++ b/src/model.h @@ -71,6 +71,7 @@ struct Model { [[nodiscard]] std::vector search_text(const std::string&) const; void search_and_replace(const std::string&); std::optional find_next_str(std::string_view); + void dedent_curr_line(); }; #endif // MODEL_H From 91e931a01252038a8bdb5bc6bc497e9612199658 Mon Sep 17 00:00:00 2001 From: Ttibsi Date: Mon, 23 Mar 2026 09:37:55 +0000 Subject: [PATCH 2/7] unit test --- tests/model_test.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/model_test.cpp b/tests/model_test.cpp index 3cbc379..793975b 100644 --- a/tests/model_test.cpp +++ b/tests/model_test.cpp @@ -702,3 +702,19 @@ TEST_CASE("find_next_str", "[model]") { REQUIRE(m.search_str == "line"); } } + +TEST_CASE("dedent_curr_line", "[model]") { + auto m = Model({" foo", "bar"}, ""); + + // Dedent a line that has some indentation + m.dedent_curr_line(); + REQUIRE(m.buf.at(0).size() == 3); + REQUIRE(m.buf.at(1).size() == 3); + REQUIRE(m.buf.at(0).at(0) == 'f'); + + // Dedent a line that has no indentation + m.dedent_curr_line(); + REQUIRE(m.buf.at(0).size() == 3); + REQUIRE(m.buf.at(1).size() == 3); + REQUIRE(m.buf.at(0).at(0) == 'f'); +} From 000e2144730e184b08eb97e610b1a4e2fb3f1788 Mon Sep 17 00:00:00 2001 From: Ttibsi Date: Mon, 23 Mar 2026 10:18:30 +0000 Subject: [PATCH 3/7] add integration test --- src/action.h | 4 ++++ src/model.cpp | 3 ++- tests/integration/normal_mode_test.py | 11 +++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/action.h b/src/action.h index 696e13e..2a143ae 100644 --- a/src/action.h +++ b/src/action.h @@ -113,6 +113,10 @@ template case ActionType::DedentLine: { auto logger = spdlog::get("basic_logger"); if (logger != nullptr) { logger->info("Action called: DedentLine"); } + + v->get_active_model()->undo_stack.push_back( + Change(ActionType::DedentLine, v->get_active_model()->current_line, ' ')); + v->get_active_model()->dedent_curr_line(); } break; diff --git a/src/model.cpp b/src/model.cpp index aa7d76b..d6943ac 100644 --- a/src/model.cpp +++ b/src/model.cpp @@ -531,7 +531,8 @@ std::optional Model::find_next_str(std::string_view sv) { void Model::dedent_curr_line() { auto offset = buf.at(current_line).find_first_not_of(' '); - if (offset == std::string::npos) { return; } + if (offset == 0) { return; } + unsaved = true; const std::size_t to_delete = std::min(4ul, offset); const std::size_t line_size = buf.at(current_line).size(); diff --git a/tests/integration/normal_mode_test.py b/tests/integration/normal_mode_test.py index 4001ad9..f2dcd3a 100644 --- a/tests/integration/normal_mode_test.py +++ b/tests/integration/normal_mode_test.py @@ -532,3 +532,14 @@ def test_e_key(r: TmuxRunner): r.press("e") assert r.statusbar_parts()[-1] == "1:7" + + +@setup("tests/fixture/test_file_1.txt") +def test_left_chevron_key(r: TmuxRunner): + r.press('<') + assert "[X]" not in r.statusbar_parts() + + r.press('j') + r.press('<') + assert "[X]" in r.statusbar_parts() + assert "2\u2502here" in r.lines()[1] From ff0e3e07a0cf0b5e58f57b541c6c3a0589da9b58 Mon Sep 17 00:00:00 2001 From: Ttibsi Date: Tue, 24 Mar 2026 08:32:17 +0000 Subject: [PATCH 4/7] Update changelog --- CHANGELOG.md | 2 ++ README.md | 1 + 2 files changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7028c74..b569248 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ### main / head +* `<` now dedents current line + ### v0.0.3 | 17/Mar/2026 * Use `;e` to open a different file from within iris * Keyboard commands `tt`, `tn`, `tp` to create and switch between tabs diff --git a/README.md b/README.md index f6527c3..c491e62 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ perform the following actions (alphabetically ordered): | ] | Go to end of paragraph (next empty line) | | _ | Go to beginning of line | | $ | Go to end of line | +| < | Dedent current line | ### Command mode Likewise, you can switch to `command` mode with the semicolon `;` key. The From 0d13f70bf82df7032581b110953e754768f58291 Mon Sep 17 00:00:00 2001 From: Ttibsi Date: Fri, 10 Apr 2026 14:55:49 +0000 Subject: [PATCH 5/7] pre-commit and resolve issue with cursor movement in find_prev --- .pre-commit-config.yaml | 6 +++--- src/model.cpp | 3 ++- tests/model_test.cpp | 7 +++++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c435a05..6b68f43 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,7 +13,7 @@ repos: exclude: "tests/fixture/no_newline_file.txt" - id: trailing-whitespace - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v21.1.8 + rev: v22.1.3 hooks: - id: clang-format exclude: ".json" @@ -41,11 +41,11 @@ repos: hooks: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.19.1 + rev: v1.20.0 hooks: - id: mypy - repo: https://github.com/rhysd/actionlint - rev: v1.7.10 + rev: v1.7.12 hooks: - id: actionlint - repo: https://github.com/sco1/brie-commit diff --git a/src/model.cpp b/src/model.cpp index d6943ac..e4283db 100644 --- a/src/model.cpp +++ b/src/model.cpp @@ -277,7 +277,8 @@ void Model::toggle_case() { // line is a relative value, char is an absolute value return rawterm::Pos( - {static_cast(current_line - cur_line), static_cast(cur_char)}); + static_cast(current_line - cur_line), + static_cast(cur_char < 0 ? current_char : cur_char)); } } diff --git a/tests/model_test.cpp b/tests/model_test.cpp index 793975b..e3585d5 100644 --- a/tests/model_test.cpp +++ b/tests/model_test.cpp @@ -343,11 +343,14 @@ TEST_CASE("find_prev", "[model]") { REQUIRE(ret.value().horizontal == 5); m.current_line -= static_cast(ret.value().vertical); - m.current_char -= static_cast(ret.value().horizontal); + m.current_char = static_cast(ret.value().horizontal); + REQUIRE(m.current_line == 5); + REQUIRE(m.current_char == 5); + REQUIRE(m.buf.at(5).at(5) == 'f'); ret = m.find_prev('t'); REQUIRE(ret.has_value()); - REQUIRE(ret.value().vertical == 3); + REQUIRE(ret.value().vertical == 2); REQUIRE(ret.value().horizontal == 5); m.current_line -= static_cast(ret.value().vertical); From 40ad3570417d505c36e10dcf07d78b1834f377c1 Mon Sep 17 00:00:00 2001 From: ttibsi Date: Fri, 10 Apr 2026 20:51:43 +0100 Subject: [PATCH 6/7] resolve further errors~# --- run.py | 2 +- src/constants.h | 2 +- src/model.cpp | 28 +++++++++++++++------------- tests/model_test.cpp | 4 ++-- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/run.py b/run.py index f357dd3..46846cd 100755 --- a/run.py +++ b/run.py @@ -160,7 +160,7 @@ def test(testname: str | None, asan: bool) -> int: create_read_only_file() testname = testname if testname is not None else "" - test_flags: str = "-sr compact --order rand" + test_flags: str = "-r compact --order rand" shell_cmd: str = f"./build/tests/test_exe {test_flags} {testname}" return run_shell_cmd( diff --git a/src/constants.h b/src/constants.h index c71cf7b..5849dab 100644 --- a/src/constants.h +++ b/src/constants.h @@ -7,7 +7,7 @@ const bool LINE_NUMBERS = true; const int TAB_SIZE = 4; -const std::size_t LINE_BORDER = 80; +const std::size_t LINE_BORDER = 100; const rawterm::Color COLOR_UI_BG = rawterm::Colors::gray; const rawterm::Color COLOR_DARK_YELLOW = rawterm::Color("#ffdd33"); diff --git a/src/model.cpp b/src/model.cpp index 70aa55f..8452f96 100644 --- a/src/model.cpp +++ b/src/model.cpp @@ -264,23 +264,25 @@ void Model::toggle_case() { [[nodiscard]] std::optional Model::find_prev(const char c) { unsigned int cur_line = current_line; - int cur_char = int32_t(current_char); - - for (; cur_line && cur_line < buf.size(); cur_line--) { - if (!(cur_line == current_line)) { cur_char = int32_t(buf.at(cur_line).size() - 1); } - auto iter = std::find( - buf.at(cur_line).rbegin() + int32_t(buf.at(cur_line).size()) - cur_char, - buf.at(cur_line).rend(), c); + while (cur_line) { + if (cur_line == current_line) { + const std::string_view search_area = + std::string_view(buf.at(current_line)).substr(0, current_char); + const auto pos = search_area.find_last_of(c); + if (pos != std::string::npos) { + return rawterm::Pos(static_cast(current_line), static_cast(pos)); + } - if (iter != buf.at(cur_line).rend()) { - cur_char = int32_t(std::distance(buf.at(cur_line).begin(), iter.base() - 1)); + } else { + const auto pos = buf.at(cur_line).find_last_of(c); - // line is a relative value, char is an absolute value - return rawterm::Pos( - static_cast(current_line - cur_line), - static_cast(cur_char < 0 ? current_char : cur_char)); + if (pos != std::string::npos) { + return rawterm::Pos(static_cast(cur_line), static_cast(pos)); + } } + + cur_line--; } return {}; diff --git a/tests/model_test.cpp b/tests/model_test.cpp index 6cd0b67..bbd937a 100644 --- a/tests/model_test.cpp +++ b/tests/model_test.cpp @@ -339,10 +339,10 @@ TEST_CASE("find_prev", "[model]") { auto ret = m.find_prev('f'); REQUIRE(ret.has_value()); - REQUIRE(ret.value().vertical == 0); + REQUIRE(ret.value().vertical == 5); REQUIRE(ret.value().horizontal == 5); - m.current_line -= static_cast(ret.value().vertical); + m.current_line = static_cast(ret.value().vertical); m.current_char = static_cast(ret.value().horizontal); REQUIRE(m.current_line == 5); REQUIRE(m.current_char == 5); From 5e5059ed7dda460008d64f1df544a16b0b68110d Mon Sep 17 00:00:00 2001 From: ttibsi Date: Fri, 10 Apr 2026 22:37:31 +0100 Subject: [PATCH 7/7] ensure integration tests pass again --- src/action.h | 7 ++++--- src/model.cpp | 4 ++-- src/model.h | 4 ++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/action.h b/src/action.h index 6edbe48..71f99ca 100644 --- a/src/action.h +++ b/src/action.h @@ -400,11 +400,12 @@ template if constexpr (std::is_same_v) { auto ret = v->get_active_model()->find_prev(action.payload); if (ret.has_value()) { - for (int i = 0; i < ret.value().vertical; i++) { + const uint_t curr_char = v->get_active_model()->current_char; + const std::size_t steps = + v->get_active_model()->current_line - ret.value().vertical; + for (std::size_t i = 0; i < steps; i++) { v->cursor_up(); } - - const uint_t curr_char = v->get_active_model()->current_char; if (ret.value().horizontal > int32_t(v->get_active_model()->current_char)) { v->cursor_right(uint32_t(ret.value().horizontal) - curr_char); } else { diff --git a/src/model.cpp b/src/model.cpp index 8452f96..f49813c 100644 --- a/src/model.cpp +++ b/src/model.cpp @@ -243,7 +243,7 @@ void Model::toggle_case() { unsaved = true; } -[[nodiscard]] std::optional Model::find_next(const char c) { +[[nodiscard]] std::optional Model::find_next(const char c) const { unsigned int cur_line = current_line; int cur_char = int32_t(current_char); @@ -262,7 +262,7 @@ void Model::toggle_case() { return {}; } -[[nodiscard]] std::optional Model::find_prev(const char c) { +[[nodiscard]] std::optional Model::find_prev(const char c) const { unsigned int cur_line = current_line; while (cur_line) { diff --git a/src/model.h b/src/model.h index aec3ef7..bc0aae2 100644 --- a/src/model.h +++ b/src/model.h @@ -57,8 +57,8 @@ struct Model { [[nodiscard]] std::optional end_of_word_pos(); void replace_char(const char); void toggle_case(); - [[nodiscard]] std::optional find_next(const char); - [[nodiscard]] std::optional find_prev(const char); + [[nodiscard]] std::optional find_next(const char) const; + [[nodiscard]] std::optional find_prev(const char) const; [[nodiscard]] bool undo(const int); [[nodiscard]] char get_current_char() const; [[nodiscard]] bool redo(const int);