From dcfc5b9ec0d35f4c66b1648ded00f4f1279e0413 Mon Sep 17 00:00:00 2001 From: asepharyana Date: Wed, 15 Jul 2026 06:22:25 +0700 Subject: [PATCH] fix(state): cegah panic saat select mention dengan cursor stale Cursor bisa berpindah (Left/Right) tanpa menutup dropdown mention, sehingga mention_start jadi stale relatif ke cursor saat Enter ditekan. select_autocomplete() lalu memanggil replace_range dengan start > end dan panic (crash seluruh TUI, termasuk daemon). Tambah guard: jika cursor < mention_start atau mention_start > buffer.len(), tutup dropdown dan kembalikan false alih-alih menyambung range yang tidak valid. Tambah regression test yang mereproduksi skenario ini. --- src/app/state/misc.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/app/state/misc.rs b/src/app/state/misc.rs index e5047a1..3c70597 100644 --- a/src/app/state/misc.rs +++ b/src/app/state/misc.rs @@ -270,6 +270,17 @@ impl InputState { self.cursor = self.buffer.len(); } AutocompleteKind::FileMention => { + // Cursor movement (Left/Right) does not close the dropdown, so + // by the time Enter is pressed `mention_start` may no longer + // describe a valid range against the current cursor/buffer + // (e.g. the cursor moved left past the '@'). Splicing on a + // stale range would panic (`start > end`) or, even when it + // doesn't panic, produce a nonsensical replacement. Treat a + // stale mention context the same as "nothing selected". + if self.cursor < self.mention_start || self.mention_start > self.buffer.len() { + self.close_autocomplete(); + return false; + } let replacement = format!("@{candidate} "); self.buffer.replace_range(self.mention_start..self.cursor, &replacement); self.cursor = self.mention_start + replacement.len(); @@ -485,6 +496,21 @@ mod tests { assert_eq!(input.cursor, 8 + "@src/main.rs ".len()); } + #[test] + fn select_file_mention_with_stale_cursor_before_mention_start_does_not_panic() { + // Simulates: user typed "foo @rea" (mention_start = 4, cursor = 8, + // dropdown open), then pressed Left 5 times without closing the + // dropdown, moving the cursor to byte 3 (before the '@'). Selecting + // now must not panic on `replace_range(4..3, ...)`. + let mut input = input_with("foo @rea", 3); + input.autocomplete_candidates = vec!["src/main.rs".to_string()]; + input.autocomplete_idx = 0; + input.autocomplete_kind = AutocompleteKind::FileMention; + input.mention_start = 4; + assert!(!input.select_autocomplete()); + assert!(!input.autocomplete_visible); + } + #[test] fn select_command_still_replaces_whole_buffer() { let mut input = input_with("/mo", 3);