diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 61a59665c15e3e..286cc9db8c01cb 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -546,10 +546,10 @@ pub struct Editor { nav_history: Option, context_menu: RwLock>, mouse_context_menu: Option, - completion_tasks: Vec<(CompletionId, Task>)>, signature_help_state: SignatureHelpState, auto_signature_help: Option, find_all_references_task_sources: Vec, + completion_tasks: Option, next_completion_id: CompletionId, completion_documentation_pre_resolve_debounce: DebouncedDelay, available_code_actions: Option<(Location, Arc<[CodeAction]>)>, @@ -853,6 +853,14 @@ enum ContextMenu { CodeActions(CodeActionsMenu), } +#[derive(Debug)] +struct CompletionTasks { + id: CompletionId, + task: Task<()>, + backup_id: Option, + backup_task: Option>, +} + impl ContextMenu { fn select_first( &mut self, @@ -1891,7 +1899,7 @@ impl Editor { nav_history: None, context_menu: RwLock::new(None), mouse_context_menu: None, - completion_tasks: Default::default(), + completion_tasks: None, signature_help_state: SignatureHelpState::default(), auto_signature_help: None, find_all_references_task_sources: Vec::new(), @@ -2471,7 +2479,7 @@ impl Editor { let mut completion_menu = completion_menu.clone(); drop(context_menu); - let query = Self::completion_query(buffer, cursor_position); + let (_, query) = Self::completion_query(buffer, cursor_position); cx.spawn(move |this, mut cx| async move { completion_menu .filter(query.as_deref(), cx.background_executor().clone()) @@ -3934,10 +3942,13 @@ impl Editor { }); } - fn completion_query(buffer: &MultiBufferSnapshot, position: impl ToOffset) -> Option { + fn completion_query( + buffer: &MultiBufferSnapshot, + position: impl ToOffset, + ) -> (multi_buffer::Anchor, Option) { let offset = position.to_offset(buffer); let (word_range, kind) = buffer.surrounding_word(offset, true); - if offset > word_range.start && kind == Some(CharKind::Word) { + let query = if offset > word_range.start && kind == Some(CharKind::Word) { Some( buffer .text_for_range(word_range.start..offset) @@ -3945,7 +3956,8 @@ impl Editor { ) } else { None - } + }; + (buffer.anchor_before(word_range.start), query) } pub fn toggle_inlay_hints(&mut self, _: &ToggleInlayHints, cx: &mut ViewContext) { @@ -4192,7 +4204,8 @@ impl Editor { return; }; - let query = Self::completion_query(&self.buffer.read(cx).read(cx), position); + let (initial_position, query) = + Self::completion_query(&self.buffer.read(cx).read(cx), position); let is_followup_invoke = { let context_menu_state = self.context_menu.read(); matches!( @@ -4218,19 +4231,26 @@ impl Editor { }), trigger_kind, }; + let snapshot = buffer.read(cx).snapshot(); + let classifier = snapshot.char_classifier_at(&buffer_position); + let first_char = options + .trigger + .as_ref() + .and_then(|trigger| trigger.chars().next()); + let should_cancel_previous = first_char.is_some_and(|char| !classifier.is_word(char)); let completions = provider.completions(&buffer, buffer_position, completion_context, cx); let sort_completions = provider.sort_completions(); - let id = post_inc(&mut self.next_completion_id); + let completion_id = post_inc(&mut self.next_completion_id); + if should_cancel_previous { + self.completion_tasks = None; + } let task = cx.spawn(|this, mut cx| { - async move { - this.update(&mut cx, |this, _| { - this.completion_tasks.retain(|(task_id, _)| *task_id >= id); - })?; + let run = async move { let completions = completions.await.log_err(); let menu = if let Some(completions) = completions { let mut menu = CompletionsMenu { - id, + id: completion_id, sort_completions, initial_position: position, match_candidates: completions @@ -4253,10 +4273,25 @@ impl Editor { DebouncedDelay::new(), )), }; + let (cursor_moved, query) = this.update(&mut cx, |this, cx| { + let position = this.selections.newest_anchor().head(); + let (new_position, new_query) = + Self::completion_query(&this.buffer.read(cx).read(cx), position); + if initial_position != new_position + && this + .completion_tasks + .as_ref() + .is_some_and(|tasks| tasks.backup_id == Some(completion_id)) + { + return (true, None); + } + (false, new_query) + })?; + menu.filter(query.as_deref(), cx.background_executor().clone()) .await; - if menu.matches.is_empty() { + if menu.matches.is_empty() || cursor_moved { None } else { this.update(&mut cx, |editor, cx| { @@ -4291,7 +4326,7 @@ impl Editor { None => {} Some(ContextMenu::Completions(prev_menu)) => { - if prev_menu.id > id { + if prev_menu.id > completion_id { return; } } @@ -4299,13 +4334,28 @@ impl Editor { _ => return, } + let is_latest = this + .completion_tasks + .as_ref() + .is_some_and(|tasks| tasks.id == completion_id); + if let Some(tasks) = this.completion_tasks.take() { + if Some(completion_id) == tasks.backup_id { + this.completion_tasks = Some(CompletionTasks { + id: tasks.id, + task: tasks.task, + backup_id: None, + backup_task: None, + }); + } + } + if this.focus_handle.is_focused(cx) && menu.is_some() { let menu = menu.unwrap(); *context_menu = Some(ContextMenu::Completions(menu)); drop(context_menu); this.discard_inline_completion(false, cx); cx.notify(); - } else if this.completion_tasks.len() <= 1 { + } else if is_latest { // If there are no more completion tasks and the last menu was // empty, we should hide it. If it was already hidden, we should // also show the copilot completion when available. @@ -4318,10 +4368,25 @@ impl Editor { Ok::<_, anyhow::Error>(()) } - .log_err() + .log_err(); + async move { + run.await; + } + }); + let (backup_id, backup_task) = if let Some(tasks) = self.completion_tasks.take() { + ( + Some(tasks.backup_id.unwrap_or(tasks.id)), + Some(tasks.backup_task.unwrap_or(tasks.task)), + ) + } else { + (None, None) + }; + self.completion_tasks = Some(CompletionTasks { + id: completion_id, + task, + backup_id, + backup_task, }); - - self.completion_tasks.push((id, task)); } pub fn confirm_completion( @@ -4583,7 +4648,7 @@ impl Editor { return None; } - editor.completion_tasks.clear(); + editor.completion_tasks = None; editor.discard_inline_completion(false, cx); let task_context = tasks @@ -5191,7 +5256,7 @@ impl Editor { let excerpt_id = cursor.excerpt_id; if self.context_menu.read().is_none() - && self.completion_tasks.is_empty() + && matches!(self.completion_tasks, None) && selection.start == selection.end { if let Some(provider) = self.inline_completion_provider() { @@ -5363,7 +5428,7 @@ impl Editor { fn hide_context_menu(&mut self, cx: &mut ViewContext) -> Option { cx.notify(); - self.completion_tasks.clear(); + self.completion_tasks = None; let context_menu = self.context_menu.write().take(); if context_menu.is_some() { self.update_visible_inline_completion(cx);