Fix anchor biases for completion replacement ranges (esp slash commands) (#32262)

Michael Sloan created

Closes #32205

The issue was that in some places the end of the replacement range used
anchors with `Bias::Left` instead of `Bias::Right`. Before #31872
completions were recomputed on every change and so the anchor bias
didn't matter. After that change, the end anchor didn't move as the
user's typing. Changing it to `Bias::Right` to "stick" to the character
to the right of the cursor fixes this.

Release Notes:

- Fixes incorrect auto-completion of `/files` in text threads (Preview
Only)

Change summary

crates/agent/src/context_picker/completion_provider.rs |  2 
crates/assistant_context_editor/src/slash_command.rs   | 15 ++++++-----
crates/editor/src/editor.rs                            | 13 ++++++++--
crates/inspector_ui/src/div_inspector.rs               |  2 
4 files changed, 20 insertions(+), 12 deletions(-)

Detailed changes

crates/agent/src/context_picker/completion_provider.rs 🔗

@@ -765,7 +765,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
 
         let snapshot = buffer.read(cx).snapshot();
         let source_range = snapshot.anchor_before(state.source_range.start)
-            ..snapshot.anchor_before(state.source_range.end);
+            ..snapshot.anchor_after(state.source_range.end);
 
         let thread_store = self.thread_store.clone();
         let text_thread_store = self.text_thread_store.clone();

crates/assistant_context_editor/src/slash_command.rs 🔗

@@ -238,13 +238,14 @@ impl SlashCommandCompletionProvider {
 
                 Ok(vec![project::CompletionResponse {
                     completions,
-                    is_incomplete: false,
+                    // TODO: Could have slash commands indicate whether their completions are incomplete.
+                    is_incomplete: true,
                 }])
             })
         } else {
             Task::ready(Ok(vec![project::CompletionResponse {
                 completions: Vec::new(),
-                is_incomplete: false,
+                is_incomplete: true,
             }]))
         }
     }
@@ -273,17 +274,17 @@ impl CompletionProvider for SlashCommandCompletionProvider {
                     position.row,
                     call.arguments.last().map_or(call.name.end, |arg| arg.end) as u32,
                 );
-                let command_range = buffer.anchor_after(command_range_start)
+                let command_range = buffer.anchor_before(command_range_start)
                     ..buffer.anchor_after(command_range_end);
 
                 let name = line[call.name.clone()].to_string();
                 let (arguments, last_argument_range) = if let Some(argument) = call.arguments.last()
                 {
                     let last_arg_start =
-                        buffer.anchor_after(Point::new(position.row, argument.start as u32));
+                        buffer.anchor_before(Point::new(position.row, argument.start as u32));
                     let first_arg_start = call.arguments.first().expect("we have the last element");
-                    let first_arg_start =
-                        buffer.anchor_after(Point::new(position.row, first_arg_start.start as u32));
+                    let first_arg_start = buffer
+                        .anchor_before(Point::new(position.row, first_arg_start.start as u32));
                     let arguments = call
                         .arguments
                         .into_iter()
@@ -296,7 +297,7 @@ impl CompletionProvider for SlashCommandCompletionProvider {
                     )
                 } else {
                     let start =
-                        buffer.anchor_after(Point::new(position.row, call.name.start as u32));
+                        buffer.anchor_before(Point::new(position.row, call.name.start as u32));
                     (None, start..buffer_position)
                 };
 

crates/editor/src/editor.rs 🔗

@@ -5067,10 +5067,16 @@ impl Editor {
             return;
         }
 
+        let multibuffer_snapshot = self.buffer.read(cx).read(cx);
+
         // Typically `start` == `end`, but with snippet tabstop choices the default choice is
         // inserted and selected. To handle that case, the start of the selection is used so that
         // the menu starts with all choices.
-        let position = self.selections.newest_anchor().start;
+        let position = self
+            .selections
+            .newest_anchor()
+            .start
+            .bias_right(&multibuffer_snapshot);
         if position.diff_base_anchor.is_some() {
             return;
         }
@@ -5083,8 +5089,9 @@ impl Editor {
         let buffer_snapshot = buffer.read(cx).snapshot();
 
         let query: Option<Arc<String>> =
-            Self::completion_query(&self.buffer.read(cx).read(cx), position)
-                .map(|query| query.into());
+            Self::completion_query(&multibuffer_snapshot, position).map(|query| query.into());
+
+        drop(multibuffer_snapshot);
 
         let provider = match requested_source {
             Some(CompletionsMenuSource::Normal) | None => self.completion_provider.clone(),

crates/inspector_ui/src/div_inspector.rs 🔗

@@ -715,7 +715,7 @@ fn completion_replace_range(snapshot: &BufferSnapshot, anchor: &Anchor) -> Optio
 
     if end_in_line > start_in_line {
         let replace_start = snapshot.anchor_before(line_start + start_in_line);
-        let replace_end = snapshot.anchor_before(line_start + end_in_line);
+        let replace_end = snapshot.anchor_after(line_start + end_in_line);
         Some(replace_start..replace_end)
     } else {
         None