diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 50cc458..ca59571 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 exclude: "^.github/workflows/build_artifact.yml" diff --git a/CHANGELOG.md b/CHANGELOG.md index cdc2846..6f47f13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ### main / head * `>` now intends the current line. +* `<` now dedents current line * Implement `;!` to execute a shell command from the command bar * Disabled continuous deployment in GHA. See `.github/workflows/build_artifact.yml` for details diff --git a/README.md b/README.md index a5102c8..8cfb857 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,7 @@ perform the following actions (alphabetically ordered): | _ | Go to beginning of line | | $ | Go to end of line | | > | Indent current line | +| < | Dedent current line | ### Command mode Likewise, you can switch to `command` mode with the semicolon `;` key. The 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/action.h b/src/action.h index 019848b..71f99ca 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, @@ -110,6 +111,16 @@ 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()->undo_stack.push_back( + Change(ActionType::DedentLine, v->get_active_model()->current_line, ' ')); + + v->get_active_model()->dedent_curr_line(); + } break; + case ActionType::DelCurrentChar: { if constexpr (std::is_same_v) { auto logger = spdlog::get("basic_logger"); @@ -389,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/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/controller.cpp b/src/controller.cpp index a89daae..4b758e9 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -400,6 +400,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); + // Increment line } else if (k.value() == rawterm::Key('>')) { if (is_readonly_model()) { continue; } diff --git a/src/model.cpp b/src/model.cpp index 9a15b78..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,24 +262,27 @@ 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; - 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)}); + if (pos != std::string::npos) { + return rawterm::Pos(static_cast(cur_line), static_cast(pos)); + } } + + cur_line--; } return {}; @@ -533,3 +536,13 @@ std::optional Model::find_next_str(std::string_view sv) { void Model::indent_curr_line() { if (buf.at(current_line).size()) { buf.at(current_line).insert(0, TAB_SIZE, ' '); } } + +void Model::dedent_curr_line() { + auto offset = buf.at(current_line).find_first_not_of(' '); + 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(); + buf.at(current_line) = buf.at(current_line).substr(to_delete, line_size); +} diff --git a/src/model.h b/src/model.h index 58a48c7..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); @@ -72,6 +72,7 @@ struct Model { void search_and_replace(const std::string&); std::optional find_next_str(std::string_view); void indent_curr_line(); + void dedent_curr_line(); }; #endif // MODEL_H diff --git a/tests/fixture/test_file_2.txt b/tests/fixture/test_file_2.txt index e69de29..9daeafb 100644 --- a/tests/fixture/test_file_2.txt +++ b/tests/fixture/test_file_2.txt @@ -0,0 +1 @@ +test diff --git a/tests/integration/normal_mode_test.py b/tests/integration/normal_mode_test.py index f6fb536..9c74b41 100644 --- a/tests/integration/normal_mode_test.py +++ b/tests/integration/normal_mode_test.py @@ -534,6 +534,17 @@ def test_e_key(r: TmuxRunner): 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] + + @setup("tests/fixture/test_file_1.txt") def test_right_chevron_key(r: TmuxRunner): r.press('>') diff --git a/tests/model_test.cpp b/tests/model_test.cpp index 7d36c38..bbd937a 100644 --- a/tests/model_test.cpp +++ b/tests/model_test.cpp @@ -339,15 +339,18 @@ 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_char -= static_cast(ret.value().horizontal); + 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); + 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); @@ -713,3 +716,19 @@ TEST_CASE("indent_curr_line", "[model]") { REQUIRE(m.buf.at(0).at(0) == ' '); REQUIRE(m.buf.at(0).at(4) == 'f'); } + +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'); +}