From 5a57ff10e7830dfbbc24cf72478eb9e249957826 Mon Sep 17 00:00:00 2001 From: Ttibsi Date: Fri, 28 Nov 2025 14:10:30 +0000 Subject: [PATCH 1/5] initial tests --- tests/integration/normal_mode_test.py | 11 +++++++++++ tests/model_test.cpp | 8 ++++++++ 2 files changed, 19 insertions(+) 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..d4ce6e1 100644 --- a/tests/model_test.cpp +++ b/tests/model_test.cpp @@ -264,6 +264,14 @@ TEST_CASE("prev_para_pos", "[model]") { REQUIRE(opt.value() == 2); } +TEST_CASE("end_of_word_pos", "[model]") { + REQUIRE(false); +} + +TEST_CASE("replace_char", "[model]") { + REQUIRE(false); +} + TEST_CASE("toggle_case", "[model]") { auto m = Model({"line one", "line two", "line three", "", "line four", "line five"}, ""); From 4f8fa39c8dc2c9bf72c477aeecec62a16cc28807 Mon Sep 17 00:00:00 2001 From: Ttibsi Date: Fri, 28 Nov 2025 15:15:33 +0000 Subject: [PATCH 2/5] Handle e key --- src/CMakeLists.txt | 2 +- src/action.h | 16 ++++++++++++++++ src/controller.cpp | 4 ++++ src/model.cpp | 29 +++++++++++++++++++++++++++++ src/model.h | 1 + 5 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a158cba..bdbde8a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -5,7 +5,7 @@ include(FetchContent) fetchcontent_declare( rawterm GIT_REPOSITORY https://github.com/ttibsi/rawterm - GIT_TAG main + GIT_TAG b7d0f399b236b069d99ee41382a972a2b6f63607 ) fetchcontent_makeavailable(rawterm) 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..9567492 100644 --- a/src/model.cpp +++ b/src/model.cpp @@ -216,6 +216,35 @@ 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++; + + // At end of line, don't move + if (incrementer == line_frag.size() - 1) { + return {}; + } + } + + 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); From b49685106d1e0e986f16abc4d340c25fa0d8ee1e Mon Sep 17 00:00:00 2001 From: Ttibsi Date: Fri, 28 Nov 2025 15:19:24 +0000 Subject: [PATCH 3/5] Add numbers to 'is_letter' --- src/model.cpp | 2 +- src/text_io.cpp | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/model.cpp b/src/model.cpp index 9567492..073d365 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 {}; 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(); } From 62ca0eb4785a6c41e0a566718e33bfec3bbc7495 Mon Sep 17 00:00:00 2001 From: Ttibsi Date: Wed, 7 Jan 2026 17:02:21 +0000 Subject: [PATCH 4/5] implement unit tests --- src/model.cpp | 3 +-- tests/model_test.cpp | 31 +++++++++++++++++++++++++++++-- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/model.cpp b/src/model.cpp index 073d365..e19c536 100644 --- a/src/model.cpp +++ b/src/model.cpp @@ -236,9 +236,8 @@ void Model::insert(const char c) { while (is_letter(line_frag.at(incrementer))) { incrementer++; - // At end of line, don't move if (incrementer == line_frag.size() - 1) { - return {}; + return incrementer; } } diff --git a/tests/model_test.cpp b/tests/model_test.cpp index d4ce6e1..067e3f1 100644 --- a/tests/model_test.cpp +++ b/tests/model_test.cpp @@ -265,11 +265,38 @@ TEST_CASE("prev_para_pos", "[model]") { } TEST_CASE("end_of_word_pos", "[model]") { - REQUIRE(false); + 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]") { - REQUIRE(false); + 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]") { From 4abf7db6cf7f4bea321c3cefcc56454161af72d3 Mon Sep 17 00:00:00 2001 From: Ttibsi Date: Wed, 7 Jan 2026 17:04:33 +0000 Subject: [PATCH 5/5] Update docs --- CHANGELOG.md | 1 + README.md | 3 ++- milestones.md | 2 +- src/CMakeLists.txt | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) 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/CMakeLists.txt b/src/CMakeLists.txt index bdbde8a..a158cba 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -5,7 +5,7 @@ include(FetchContent) fetchcontent_declare( rawterm GIT_REPOSITORY https://github.com/ttibsi/rawterm - GIT_TAG b7d0f399b236b069d99ee41382a972a2b6f63607 + GIT_TAG main ) fetchcontent_makeavailable(rawterm)