From 448db20eaa87ed3ccd66e40b18cd2b784cc912ac Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 7 Apr 2025 16:45:29 -0600 Subject: [PATCH] Fix bad unicode calculations in do_completion (#28259) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: João Marcos Release Notes: - Fixed a panic with completions around non-ASCII code --------- Co-authored-by: João Marcos --- crates/editor/src/editor.rs | 36 +++++++++++---------- crates/vim/src/normal/repeat.rs | 56 +++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 17 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 5c4b6b90b9f..ee53a213007 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4610,14 +4610,17 @@ impl Editor { let lookahead = old_range .end .saturating_sub(newest_selection.end.text_anchor.to_offset(buffer)); - let mut common_prefix_len = old_text - .bytes() - .zip(new_text.bytes()) - .take_while(|(a, b)| a == b) - .count(); + let mut common_prefix_len = 0; + for (a, b) in old_text.chars().zip(new_text.chars()) { + if a == b { + common_prefix_len += a.len_utf8(); + } else { + break; + } + } let snapshot = self.buffer.read(cx).snapshot(cx); - let mut range_to_replace: Option> = None; + let mut range_to_replace: Option> = None; let mut ranges = Vec::new(); let mut linked_edits = HashMap::<_, Vec<_>>::default(); for selection in &selections { @@ -4625,10 +4628,7 @@ impl Editor { let start = selection.start.saturating_sub(lookbehind); let end = selection.end + lookahead; if selection.id == newest_selection.id { - range_to_replace = Some( - ((start + common_prefix_len) as isize - selection.start as isize) - ..(end as isize - selection.start as isize), - ); + range_to_replace = Some(start + common_prefix_len..end); } ranges.push(start + common_prefix_len..end); } else { @@ -4636,12 +4636,7 @@ impl Editor { ranges.clear(); ranges.extend(selections.iter().map(|s| { if s.id == newest_selection.id { - range_to_replace = Some( - old_range.start.to_offset_utf16(&snapshot).0 as isize - - selection.start as isize - ..old_range.end.to_offset_utf16(&snapshot).0 as isize - - selection.start as isize, - ); + range_to_replace = Some(old_range.clone()); old_range.clone() } else { s.start..s.end @@ -4667,8 +4662,15 @@ impl Editor { } let text = &new_text[common_prefix_len..]; + let utf16_range_to_replace = range_to_replace.map(|range| { + let newest_selection = self.selections.newest::(cx).range(); + let selection_start_utf16 = newest_selection.start.0 as isize; + + range.start.to_offset_utf16(&snapshot).0 as isize - selection_start_utf16 + ..range.end.to_offset_utf16(&snapshot).0 as isize - selection_start_utf16 + }); cx.emit(EditorEvent::InputHandled { - utf16_range_to_replace: range_to_replace, + utf16_range_to_replace, text: text.into(), }); diff --git a/crates/vim/src/normal/repeat.rs b/crates/vim/src/normal/repeat.rs index 1ffda0b9d9f..d396d0ae4d2 100644 --- a/crates/vim/src/normal/repeat.rs +++ b/crates/vim/src/normal/repeat.rs @@ -482,6 +482,62 @@ mod test { ); } + #[gpui::test] + async fn test_repeat_completion_unicode_bug(cx: &mut gpui::TestAppContext) { + VimTestContext::init(cx); + let cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + completion_provider: Some(lsp::CompletionOptions { + trigger_characters: Some(vec![".".to_string(), ":".to_string()]), + resolve_provider: Some(true), + ..Default::default() + }), + ..Default::default() + }, + cx, + ) + .await; + let mut cx = VimTestContext::new_with_lsp(cx, true); + + cx.set_state( + indoc! {" + ĩлˇк + ĩлк + "}, + Mode::Normal, + ); + + let mut request = cx.set_request_handler::( + move |_, params, _| async move { + let position = params.text_document_position.position; + let mut to_the_left = position; + to_the_left.character -= 2; + Ok(Some(lsp::CompletionResponse::Array(vec![ + lsp::CompletionItem { + label: "oops".to_string(), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: lsp::Range::new(to_the_left, position), + new_text: "к!".to_string(), + })), + ..Default::default() + }, + ]))) + }, + ); + cx.simulate_keystrokes("i ."); + request.next().await; + cx.condition(|editor, _| editor.context_menu_visible()) + .await; + cx.simulate_keystrokes("enter escape"); + cx.assert_state( + indoc! {" + ĩкˇ!к + ĩлк + "}, + Mode::Normal, + ); + } + #[gpui::test] async fn test_repeat_visual(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await;