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);