From 833bc6979aaecaf091dcd834874d9f329856ce5a Mon Sep 17 00:00:00 2001 From: Remco Smits Date: Sat, 12 Jul 2025 20:24:49 +0200 Subject: [PATCH] debugger: Fix correctly determine replace range for debug console completions (#33959) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up #33868 This PR fixes a few issues with determining the completion range for client‑ and variable‑list completions. 1. Non‑word completions We previously supported only word characters and _, using their combined length to compute the start offset. In PHP, however, an expression can contain `$`, `-`, `>`, `[`, `]`, `(`, and `)`. Because these characters weren’t treated as word characters, the start offset stopped at them, even when the preceding character was part of a word. 2. Trailing characters inside the search text When autocompletion occurred in the middle of the search text, we didn’t account for trailing characters. As a result, the start offset was off by the number of characters after the cursor. For example, replacing res with result in print(res) produced `print(rresult)` because the trailing `)` wasn’t subtracted from the start offset. The following completions are correctly covered now: - **Before** `$aut` -> `$aut$author` **After** `$aut` -> `$author` - **Before** `$author->na` -> `$author->na$author->name` **After** `$author->na` -> `$author->name` - **Before** `$author->books[` -> `$author->books[$author->books[0]` **After** `$author->books[` -> `$author->books[0]` - **Before** `print(res)` -> `print(rresult)` **After** `print(res)` -> `print(result)` **Before** https://github.com/user-attachments/assets/b530cf31-8d4d-45e6-9650-18574f14314c https://github.com/user-attachments/assets/52475b7b-2bf2-4749-98ec-0dc933fcc364 **After** https://github.com/user-attachments/assets/c065701b-31c9-4e0a-b584-d1daffe3a38c https://github.com/user-attachments/assets/455ebb3e-632e-4a57-aea8-d214d2992c06 Release Notes: - Debugger: Fixed autocompletion not always replacing the correct search text --- .../src/session/running/console.rs | 138 +++++++++++++----- 1 file changed, 101 insertions(+), 37 deletions(-) diff --git a/crates/debugger_ui/src/session/running/console.rs b/crates/debugger_ui/src/session/running/console.rs index 9375c8820b..1385bec54e 100644 --- a/crates/debugger_ui/src/session/running/console.rs +++ b/crates/debugger_ui/src/session/running/console.rs @@ -12,7 +12,7 @@ use gpui::{ Action as _, AppContext, Context, Corner, Entity, FocusHandle, Focusable, HighlightStyle, Hsla, Render, Subscription, Task, TextStyle, WeakEntity, actions, }; -use language::{Buffer, CodeLabel, ToOffset}; +use language::{Anchor, Buffer, CodeLabel, TextBufferSnapshot, ToOffset}; use menu::{Confirm, SelectNext, SelectPrevious}; use project::{ Completion, CompletionResponse, @@ -637,27 +637,13 @@ impl ConsoleQueryBarCompletionProvider { }); let snapshot = buffer.read(cx).text_snapshot(); - let query = snapshot.text(); - let replace_range = { - let buffer_offset = buffer_position.to_offset(&snapshot); - let reversed_chars = snapshot.reversed_chars_for_range(0..buffer_offset); - let mut word_len = 0; - for ch in reversed_chars { - if ch.is_alphanumeric() || ch == '_' { - word_len += 1; - } else { - break; - } - } - let word_start_offset = buffer_offset - word_len; - let start_anchor = snapshot.anchor_at(word_start_offset, Bias::Left); - start_anchor..buffer_position - }; + let buffer_text = snapshot.text(); + cx.spawn(async move |_, cx| { const LIMIT: usize = 10; let matches = fuzzy::match_strings( &string_matches, - &query, + &buffer_text, true, true, LIMIT, @@ -672,7 +658,12 @@ impl ConsoleQueryBarCompletionProvider { let variable_value = variables.get(&string_match.string)?; Some(project::Completion { - replace_range: replace_range.clone(), + replace_range: Self::replace_range_for_completion( + &buffer_text, + buffer_position, + string_match.string.as_bytes(), + &snapshot, + ), new_text: string_match.string.clone(), label: CodeLabel { filter_range: 0..string_match.string.len(), @@ -697,6 +688,28 @@ impl ConsoleQueryBarCompletionProvider { }) } + fn replace_range_for_completion( + buffer_text: &String, + buffer_position: Anchor, + new_bytes: &[u8], + snapshot: &TextBufferSnapshot, + ) -> Range { + let buffer_offset = buffer_position.to_offset(&snapshot); + let buffer_bytes = &buffer_text.as_bytes()[0..buffer_offset]; + + let mut prefix_len = 0; + for i in (0..new_bytes.len()).rev() { + if buffer_bytes.ends_with(&new_bytes[0..i]) { + prefix_len = i; + break; + } + } + + let start = snapshot.clip_offset(buffer_offset - prefix_len, Bias::Left); + + snapshot.anchor_before(start)..buffer_position + } + const fn completion_type_score(completion_type: CompletionItemType) -> usize { match completion_type { CompletionItemType::Field | CompletionItemType::Property => 0, @@ -744,6 +757,8 @@ impl ConsoleQueryBarCompletionProvider { cx.background_executor().spawn(async move { let completions = completion_task.await?; + let buffer_text = snapshot.text(); + let completions = completions .into_iter() .map(|completion| { @@ -753,26 +768,14 @@ impl ConsoleQueryBarCompletionProvider { .as_ref() .unwrap_or(&completion.label) .to_owned(); - let buffer_text = snapshot.text(); - let buffer_bytes = buffer_text.as_bytes(); - let new_bytes = new_text.as_bytes(); - - let mut prefix_len = 0; - for i in (0..new_bytes.len()).rev() { - if buffer_bytes.ends_with(&new_bytes[0..i]) { - prefix_len = i; - break; - } - } - - let buffer_offset = buffer_position.to_offset(&snapshot); - let start = buffer_offset - prefix_len; - let start = snapshot.clip_offset(start, Bias::Left); - let start = snapshot.anchor_before(start); - let replace_range = start..buffer_position; project::Completion { - replace_range, + replace_range: Self::replace_range_for_completion( + &buffer_text, + buffer_position, + new_text.as_bytes(), + &snapshot, + ), new_text, label: CodeLabel { filter_range: 0..completion.label.len(), @@ -944,3 +947,64 @@ fn color_fetcher(color: ansi::Color) -> fn(&Theme) -> Hsla { }; color_fetcher } + +#[cfg(test)] +mod tests { + use super::*; + use crate::tests::init_test; + use editor::test::editor_test_context::EditorTestContext; + use gpui::TestAppContext; + use language::Point; + + #[track_caller] + fn assert_completion_range( + input: &str, + expect: &str, + replacement: &str, + cx: &mut EditorTestContext, + ) { + cx.set_state(input); + + let buffer_position = + cx.editor(|editor, _, cx| editor.selections.newest::(cx).start); + + let snapshot = &cx.buffer_snapshot(); + + let replace_range = ConsoleQueryBarCompletionProvider::replace_range_for_completion( + &cx.buffer_text(), + snapshot.anchor_before(buffer_position), + replacement.as_bytes(), + &snapshot, + ); + + cx.update_editor(|editor, _, cx| { + editor.edit( + vec![( + snapshot.offset_for_anchor(&replace_range.start) + ..snapshot.offset_for_anchor(&replace_range.end), + replacement, + )], + cx, + ); + }); + + pretty_assertions::assert_eq!(expect, cx.display_text()); + } + + #[gpui::test] + async fn test_determine_completion_replace_range(cx: &mut TestAppContext) { + init_test(cx); + + let mut cx = EditorTestContext::new(cx).await; + + assert_completion_range("resˇ", "result", "result", &mut cx); + assert_completion_range("print(resˇ)", "print(result)", "result", &mut cx); + assert_completion_range("$author->nˇ", "$author->name", "$author->name", &mut cx); + assert_completion_range( + "$author->books[ˇ", + "$author->books[0]", + "$author->books[0]", + &mut cx, + ); + } +}