diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f47f13..61759b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ * Disabled continuous deployment in GHA. See `.github/workflows/build_artifact.yml` for details +* Resolved crash when the user tries to `delete word` past the end of a 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/src/action.h b/src/action.h index 71f99ca..e6a9455 100644 --- a/src/action.h +++ b/src/action.h @@ -156,14 +156,14 @@ template auto logger = spdlog::get("basic_logger"); if (logger != nullptr) { logger->info("Action called: DelCurrentWord"); } - const WordPos word = v->get_active_model()->current_word(); + const std::optional word = v->get_active_model()->current_word(); + if (!word.has_value()) { break; } v->get_active_model()->undo_stack.push_back(Change( - ActionType::DelCurrentWord, v->get_active_model()->current_line, word.start_pos, - word.text)); - - v->get_active_model()->delete_current_word(word); + ActionType::DelCurrentWord, v->get_active_model()->current_line, + word.value().start_pos, word.value().text)); + v->get_active_model()->delete_current_word(word.value()); } break; case ActionType::EndOfLine: { diff --git a/src/model.cpp b/src/model.cpp index f49813c..6d22161 100644 --- a/src/model.cpp +++ b/src/model.cpp @@ -447,11 +447,14 @@ void Model::delete_current_line() { } } -[[nodiscard]] const WordPos Model::current_word() const { +[[nodiscard]] const std::optional Model::current_word() const { WordPos ret = {"", 0, 0}; const std::string* cur_line = &buf.at(current_line); uint_t start = current_char; + // "Start" is already at the end of the line + if (start == cur_line->size()) { return {}; } + while (start && is_letter(cur_line->at(start))) { start--; } @@ -471,6 +474,7 @@ void Model::delete_current_line() { } void Model::delete_current_word(const WordPos pos) { + unsaved = true; buf.at(pos.lineno).erase(pos.start_pos, pos.text.size()); } diff --git a/src/model.h b/src/model.h index bc0aae2..4d705ca 100644 --- a/src/model.h +++ b/src/model.h @@ -66,7 +66,7 @@ struct Model { void move_line_up(); void set_read_only(std::string_view); void delete_current_line(); - [[nodiscard]] const WordPos current_word() const; + [[nodiscard]] const std::optional current_word() const; void delete_current_word(const WordPos); [[nodiscard]] std::vector search_text(const std::string&) const; void search_and_replace(const std::string&); diff --git a/tests/fixture/test_file_2.txt b/tests/fixture/test_file_2.txt index 9daeafb..e69de29 100644 --- a/tests/fixture/test_file_2.txt +++ b/tests/fixture/test_file_2.txt @@ -1 +0,0 @@ -test diff --git a/tests/integration/normal_mode_test.py b/tests/integration/normal_mode_test.py index 9c74b41..45a184d 100644 --- a/tests/integration/normal_mode_test.py +++ b/tests/integration/normal_mode_test.py @@ -523,6 +523,15 @@ def test_dw_key(r: TmuxRunner): assert "Lorem ipsum" not in r.lines()[0] +@setup("tests/fixture/test_file_1.txt") +def test_dw_key_end_of_file(r: TmuxRunner): + r.type_str("Gwwel") + r.type_str("dw") + + assert r.MODIFIED_MARKER not in r.statusbar_parts() + assert "newline" in r.lines()[2] + + @setup("tests/fixture/test_file_1.txt") def test_e_key(r: TmuxRunner): assert r.statusbar_parts()[-1] == "1:1" diff --git a/tests/integration/setup.py b/tests/integration/setup.py index eb129fa..6d7b1ad 100644 --- a/tests/integration/setup.py +++ b/tests/integration/setup.py @@ -15,6 +15,7 @@ class TmuxRunner(Runner): CMD_KEY: Final[str] = "\\;" SELECTED_LINE_ANSI: Final[str] = "\x1b[38;2;255;221;51m" + MODIFIED_MARKER: Final[str] = "[X]" filename: str def __init__(self, *args, **kwargs) -> None: diff --git a/tests/model_test.cpp b/tests/model_test.cpp index bbd937a..1c621c7 100644 --- a/tests/model_test.cpp +++ b/tests/model_test.cpp @@ -461,7 +461,7 @@ TEST_CASE("undo", "[model]") { m.current_char = 1; m.undo_stack.push_back(Change(ActionType::DelCurrentWord, 1, 0, "line")); - m.delete_current_word(m.current_word()); + m.delete_current_word(m.current_word().value()); REQUIRE(m.buf.at(1) == " two"); REQUIRE(m.undo(24)); @@ -553,7 +553,7 @@ TEST_CASE("redo", "[model]") { m.current_char = 1; m.undo_stack.push_back(Change(ActionType::DelCurrentWord, 1, 0, "line")); - m.delete_current_word(m.current_word()); + m.delete_current_word(m.current_word().value()); REQUIRE(m.buf.at(1) == " two"); REQUIRE(m.undo(24)); @@ -617,19 +617,21 @@ TEST_CASE("current_word", "[model]") { m.current_line = 1; m.current_char = 2; - const WordPos ret = m.current_word(); - REQUIRE(ret.text == "line"); - REQUIRE(ret.start_pos == 0); + const std::optional ret = m.current_word(); + REQUIRE(ret.has_value()); + REQUIRE(ret.value().text == "line"); + REQUIRE(ret.value().start_pos == 0); } SECTION("Search to end of line") { m.current_line = 1; m.current_char = 6; - const WordPos ret = m.current_word(); + const std::optional ret = m.current_word(); + REQUIRE(ret.has_value()); REQUIRE(m.buf.at(m.current_line).at(m.current_char) == 'w'); - REQUIRE(ret.text == "two"); - REQUIRE(ret.start_pos == 5); + REQUIRE(ret.value().text == "two"); + REQUIRE(ret.value().start_pos == 5); } } @@ -638,13 +640,13 @@ TEST_CASE("delete_current_word", "[model]") { m.current_line = 1; m.current_char = 2; - m.delete_current_word(m.current_word()); + m.delete_current_word(m.current_word().value()); REQUIRE(m.buf.at(1) == " two"); m.current_line = 2; m.current_char = 7; - m.delete_current_word(m.current_word()); + m.delete_current_word(m.current_word().value()); REQUIRE(m.buf.at(2) == "line "); }