debugger: Fix correctly determine replace range for debug console completions (#33959)

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
This commit is contained in:
Remco Smits 2025-07-12 20:24:49 +02:00 committed by GitHub
parent a8cc927303
commit 833bc6979a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -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<Anchor> {
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::<Point>(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,
);
}
}