diff --git a/CHANGELOG.md b/CHANGELOG.md index 3020216..af6393f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ moves back that amount of space * Added `;f` command to find a given string further on in the buffer * Iris now handles horizontal scrolling when a line is longer than the screen * Allow for specifying file to save to from command bar +* `e` key now moves cursor to end of the current word * Resolved issue with iris crashing after opening an existing file with 0 bytes * Resolved issue where filename isn't centered in the status bar diff --git a/README.md b/README.md index 7d1eed4..f6527c3 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,8 @@ perform the following actions (alphabetically ordered): | b | Move cursor back one word | | dl | Delete the current line | | dw | Delete the current word | -| f | Find next entered char ahead in file | +| e | Find next entered char ahead in file | +| f | Move cursor to end of current word | | F | Find next entered char back in file | | g | Go to top of file | | G | Go to bottom of file | diff --git a/milestones.md b/milestones.md index 81b7307..092b579 100644 --- a/milestones.md +++ b/milestones.md @@ -72,5 +72,5 @@ * [x] `w` (delete word) * [x] `J` * [x] `K` -* [ ] `e` +* [x] `e` * [ ] `>` / `<` (indentation) diff --git a/src/action.h b/src/action.h index 28c6973..67a74f1 100644 --- a/src/action.h +++ b/src/action.h @@ -19,6 +19,7 @@ enum class ActionType { DelCurrentLine, DelCurrentWord, EndOfLine, + JumpEndOfWord, JumpNextPara, JumpPrevPara, JumpNextWord, @@ -170,6 +171,21 @@ template v->cursor_end_of_line(); } break; + case ActionType::JumpEndOfWord: { + auto logger = spdlog::get("basic_logger"); + if (logger != nullptr) { + logger->info("Action called: JumpEndOfWord"); + } + + std::optional count = v->get_active_model()->end_of_word_pos(); + if (count.has_value()) { + for (int i = 0; i < count.value(); i++) { + v->cursor_right(); + } + } + + } break; + case ActionType::JumpNextPara: { auto logger = spdlog::get("basic_logger"); if (logger != nullptr) { diff --git a/src/controller.cpp b/src/controller.cpp index 81f6c67..3f5c8a9 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -211,6 +211,10 @@ void Controller::start_action_engine() { continue; } + // Go to end of current word + } else if (k.value() == rawterm::Key('e')) { + parse_action(&view, Action {ActionType::JumpEndOfWord}); + // find forward } else if (k.value() == rawterm::Key('f')) { auto k2 = rawterm::wait_for_input(); diff --git a/src/model.cpp b/src/model.cpp index ebd8527..e19c536 100644 --- a/src/model.cpp +++ b/src/model.cpp @@ -113,7 +113,7 @@ void Model::insert(const char c) { return (idx < static_cast(buf.size()) && idx >= 0); } -// Word (noun) - a sequence of characters that match regex A-Za-z +// Word (noun) - a sequence of characters that match regex A-Za-z0-9 [[nodiscard]] std::optional Model::next_word_pos() { if (current_char == buf.at(current_line).size() - 1) { return {}; @@ -216,6 +216,34 @@ void Model::insert(const char c) { return distance; } +[[nodiscard]] std::optional Model::end_of_word_pos() { + if (current_char == buf.at(current_line).size() - 1) { + return {}; + } else if (buf.at(current_line).empty()) { + return {}; + } + + uint incrementer = 0; + std::string_view line_frag = + std::string_view(buf.at(current_line)).substr(current_char, buf.at(current_line).size()); + + // If the _next_ char is a space, we want to go past that + if (line_frag.at(1) == ' ') { + incrementer += 2; + }; + + // go to end of current "word" + while (is_letter(line_frag.at(incrementer))) { + incrementer++; + + if (incrementer == line_frag.size() - 1) { + return incrementer; + } + } + + return --incrementer; +} + void Model::replace_char(const char c) { if (buf.at(current_line).empty()) { buf.at(current_line).push_back(c); diff --git a/src/model.h b/src/model.h index 2f538f5..d07f2f8 100644 --- a/src/model.h +++ b/src/model.h @@ -54,6 +54,7 @@ struct Model { [[nodiscard]] std::optional prev_word_pos(); [[nodiscard]] std::optional next_para_pos(); [[nodiscard]] std::optional prev_para_pos(); + [[nodiscard]] std::optional end_of_word_pos(); void replace_char(const char); void toggle_case(); [[nodiscard]] std::optional find_next(const char); diff --git a/src/text_io.cpp b/src/text_io.cpp index efc488b..e18ac71 100644 --- a/src/text_io.cpp +++ b/src/text_io.cpp @@ -103,11 +103,13 @@ void rtrim(std::string& str) { return result; } +// TODO: Investigate if symbols should count here [[nodiscard]] bool is_letter(const char& c) { - std::array alphabet = { - 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', - 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', - 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'}; + std::array alphabet = { + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', + 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', + 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', + 'W', 'X', 'Y', 'Z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}; return std::find(alphabet.begin(), alphabet.end(), c) != alphabet.end(); } diff --git a/tests/integration/normal_mode_test.py b/tests/integration/normal_mode_test.py index c998d22..bffc955 100644 --- a/tests/integration/normal_mode_test.py +++ b/tests/integration/normal_mode_test.py @@ -521,3 +521,14 @@ def test_dw_key(r: TmuxRunner): r.type_str("dw") # delete word assert "Lorem ipsum" not in r.lines()[0] + + +@setup("tests/fixture/test_file_1.txt") +def test_e_key(r: TmuxRunner): + assert r.statusbar_parts()[-1] == "1:1" + + r.press("e") + assert r.statusbar_parts()[-1] == "1:4" + + r.press("e") + assert r.statusbar_parts()[-1] == "1:7" diff --git a/tests/model_test.cpp b/tests/model_test.cpp index 79a566e..067e3f1 100644 --- a/tests/model_test.cpp +++ b/tests/model_test.cpp @@ -264,6 +264,41 @@ TEST_CASE("prev_para_pos", "[model]") { REQUIRE(opt.value() == 2); } +TEST_CASE("end_of_word_pos", "[model]") { + auto m = Model({"line one", "", "line five"}, ""); + + m.current_char = 7; + REQUIRE(m.end_of_word_pos() == std::nullopt); + + m.current_char = 0; + m.current_line = 1; + REQUIRE(m.end_of_word_pos() == std::nullopt); + + m.current_line = 0; + REQUIRE(m.end_of_word_pos().has_value()); + REQUIRE(m.end_of_word_pos().value() == 3); + + m.current_char = 3; + REQUIRE(m.end_of_word_pos().has_value()); + REQUIRE(m.end_of_word_pos().value() == 4); +} + +TEST_CASE("replace_char", "[model]") { + auto m = Model({"line one", "", "line five"}, ""); + + m.current_line = 1; + m.replace_char('c'); + REQUIRE(m.buf.at(1).size() == 1); + REQUIRE(m.buf.at(1) == "c"); + + m.current_line = 2; + m.current_char = 4; + m.replace_char('_'); + REQUIRE(m.buf.at(2).size() == 9); + REQUIRE(m.buf.at(2).at(4) == '_'); + REQUIRE(m.buf.at(2) == "line_five"); +} + TEST_CASE("toggle_case", "[model]") { auto m = Model({"line one", "line two", "line three", "", "line four", "line five"}, "");