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');
+}