diff --git a/crates/editor/src/code_actions.rs b/crates/editor/src/code_actions.rs new file mode 100644 index 0000000000000000000000000000000000000000..a5d33926d0c473bc38dbf97f879f11695abd4128 --- /dev/null +++ b/crates/editor/src/code_actions.rs @@ -0,0 +1,523 @@ +use super::*; + +impl Editor { + /// Toggles an action selection menu for the latest selection. + /// May show LSP code actions, code lens' command, runnables and potentially more entities applicable as actions. + /// Previous menu toggled with this method will be closed. + pub fn toggle_code_actions( + &mut self, + action: &ToggleCodeActions, + window: &mut Window, + cx: &mut Context, + ) { + let quick_launch = action.quick_launch; + let mut context_menu = self.context_menu.borrow_mut(); + if let Some(CodeContextMenu::CodeActions(code_actions)) = context_menu.as_ref() { + if code_actions.deployed_from == action.deployed_from { + // Toggle if we're selecting the same one + *context_menu = None; + cx.notify(); + return; + } else { + // Otherwise, clear it and start a new one + *context_menu = None; + cx.notify(); + } + } + drop(context_menu); + let snapshot = self.snapshot(window, cx); + let deployed_from = action.deployed_from.clone(); + let action = action.clone(); + self.completion_tasks.clear(); + self.discard_edit_prediction(EditPredictionDiscardReason::Ignored, cx); + + let multibuffer_point = match &action.deployed_from { + Some(CodeActionSource::Indicator(row)) | Some(CodeActionSource::RunMenu(row)) => { + DisplayPoint::new(*row, 0).to_point(&snapshot) + } + _ => self + .selections + .newest::(&snapshot.display_snapshot) + .head(), + }; + let Some((buffer, buffer_row)) = snapshot + .buffer_snapshot() + .buffer_line_for_row(MultiBufferRow(multibuffer_point.row)) + .and_then(|(buffer_snapshot, range)| { + self.buffer() + .read(cx) + .buffer(buffer_snapshot.remote_id()) + .map(|buffer| (buffer, range.start.row)) + }) + else { + return; + }; + let buffer_id = buffer.read(cx).remote_id(); + let tasks = self + .runnables + .runnables((buffer_id, buffer_row)) + .map(|t| Arc::new(t.to_owned())); + + let project = self.project.clone(); + let runnable_task = match deployed_from { + Some(CodeActionSource::Indicator(_)) => Task::ready(Ok(Default::default())), + _ => { + let mut task_context_task = Task::ready(Ok(None)); + let workspace = self.workspace().map(|w| w.downgrade()); + if let Some(tasks) = &tasks + && let Some(project) = project + { + task_context_task = + Self::build_tasks_context(&project, &buffer, buffer_row, tasks, cx); + } + + cx.spawn_in(window, { + let buffer = buffer.clone(); + async move |editor, cx| { + let task_context = match workspace { + Some(ws) => task_context_task + .await + .notify_workspace_async_err(ws, cx) + .flatten(), + None => task_context_task.await.ok().flatten(), + }; + + let resolved_tasks = + tasks + .zip(task_context.clone()) + .map(|(tasks, task_context)| ResolvedTasks { + templates: tasks.resolve(&task_context).collect(), + position: snapshot.buffer_snapshot().anchor_before(Point::new( + multibuffer_point.row, + tasks.column, + )), + }); + let debug_scenarios = editor + .update(cx, |editor, cx| { + editor.debug_scenarios(&resolved_tasks, &buffer, cx) + })? + .await; + anyhow::Ok((resolved_tasks, debug_scenarios, task_context)) + } + }) + } + }; + + let toggle_task = cx.spawn_in(window, async move |editor, cx| { + let (resolved_tasks, debug_scenarios, task_context) = runnable_task.await?; + + let code_actions = if let Some(CodeActionSource::RunMenu(_)) = &deployed_from { + None + } else { + editor.update(cx, |editor, _cx| match &editor.code_actions_for_selection { + CodeActionsForSelection::None => None, + CodeActionsForSelection::Fetching(task) => Some(task.clone()), + CodeActionsForSelection::Ready(action_fetch_ready) => { + Some(Task::ready(Some(action_fetch_ready.clone())).shared()) + } + })? + }; + let code_actions = match code_actions { + Some(code_actions) => code_actions + .await + .filter(|ActionFetchReady { location, .. }| { + let snapshot = location.buffer.read_with(cx, |buffer, _| buffer.snapshot()); + let point_range = location.range.to_point(&snapshot); + (point_range.start.row..=point_range.end.row).contains(&buffer_row) + }) + .map(|ActionFetchReady { actions, .. }| actions), + None => None, + }; + + editor.update_in(cx, |editor, window, cx| { + let spawn_straight_away = quick_launch + && resolved_tasks + .as_ref() + .is_some_and(|tasks| tasks.templates.len() == 1) + && code_actions + .as_ref() + .is_none_or(|actions| actions.is_empty()) + && debug_scenarios.is_empty(); + + crate::hover_popover::hide_hover(editor, cx); + let actions = CodeActionContents::new( + resolved_tasks, + code_actions, + debug_scenarios, + task_context.unwrap_or_default(), + ); + + // Don't show the menu if there are no actions available + if actions.is_empty() { + cx.notify(); + return Task::ready(Ok(())); + } + + *editor.context_menu.borrow_mut() = + Some(CodeContextMenu::CodeActions(CodeActionsMenu { + buffer, + actions, + selected_item: Default::default(), + scroll_handle: UniformListScrollHandle::default(), + deployed_from, + })); + cx.notify(); + if spawn_straight_away + && let Some(task) = editor.confirm_code_action( + &ConfirmCodeAction { item_ix: Some(0) }, + window, + cx, + ) + { + return task; + } + + Task::ready(Ok(())) + }) + }); + self.runnables_for_selection_toggle = cx.background_spawn(async move { + match toggle_task.await { + Ok(code_action_spawn) => match code_action_spawn.await { + Ok(()) => {} + Err(e) => log::error!("failed to spawn a toggled code action: {e:#}"), + }, + Err(e) => log::error!("failed to toggle code actions: {e:#}"), + } + }) + } + + pub fn confirm_code_action( + &mut self, + action: &ConfirmCodeAction, + window: &mut Window, + cx: &mut Context, + ) -> Option>> { + if self.read_only(cx) { + return None; + } + + let actions_menu = + if let CodeContextMenu::CodeActions(menu) = self.hide_context_menu(window, cx)? { + menu + } else { + return None; + }; + + let action_ix = action.item_ix.unwrap_or(actions_menu.selected_item); + let action = actions_menu.actions.get(action_ix)?; + let title = action.label(); + let buffer = actions_menu.buffer; + let workspace = self.workspace()?; + + match action { + CodeActionsItem::Task(task_source_kind, resolved_task) => { + workspace.update(cx, |workspace, cx| { + workspace.schedule_resolved_task( + task_source_kind, + resolved_task, + false, + window, + cx, + ); + + Some(Task::ready(Ok(()))) + }) + } + CodeActionsItem::CodeAction { action, provider } => { + if code_lens::try_handle_client_command(&action, self, &workspace, window, cx) { + return Some(Task::ready(Ok(()))); + } + + let apply_code_action = + provider.apply_code_action(buffer, action, true, window, cx); + let workspace = workspace.downgrade(); + Some(cx.spawn_in(window, async move |editor, cx| { + let project_transaction = apply_code_action.await?; + Self::open_project_transaction( + &editor, + workspace, + project_transaction, + title, + cx, + ) + .await + })) + } + CodeActionsItem::DebugScenario(scenario) => { + let context = actions_menu.actions.context.into(); + + workspace.update(cx, |workspace, cx| { + dap::send_telemetry(&scenario, TelemetrySpawnLocation::Gutter, cx); + workspace.start_debug_session( + scenario, + context, + Some(buffer), + None, + window, + cx, + ); + }); + Some(Task::ready(Ok(()))) + } + } + } + + pub fn code_actions_enabled_for_toolbar(&self, cx: &App) -> bool { + !self.code_action_providers.is_empty() + && EditorSettings::get_global(cx).toolbar.code_actions + } + + pub fn has_available_code_actions_for_selection(&self) -> bool { + if let CodeActionsForSelection::Ready(ready) = &self.code_actions_for_selection { + !ready.actions.is_empty() + } else { + false + } + } + + pub fn context_menu(&self) -> &RefCell> { + &self.context_menu + } + + pub(super) fn render_inline_code_actions( + &self, + icon_size: ui::IconSize, + display_row: DisplayRow, + is_active: bool, + cx: &mut Context, + ) -> AnyElement { + let show_tooltip = !self.context_menu_visible(); + IconButton::new("inline_code_actions", ui::IconName::BoltFilled) + .icon_size(icon_size) + .shape(ui::IconButtonShape::Square) + .icon_color(ui::Color::Hidden) + .toggle_state(is_active) + .when(show_tooltip, |this| { + this.tooltip({ + let focus_handle = self.focus_handle.clone(); + move |_window, cx| { + Tooltip::for_action_in( + "Toggle Code Actions", + &ToggleCodeActions { + deployed_from: None, + quick_launch: false, + }, + &focus_handle, + cx, + ) + } + }) + }) + .on_click(cx.listener(move |editor, _: &ClickEvent, window, cx| { + window.focus(&editor.focus_handle(cx), cx); + editor.toggle_code_actions( + &crate::actions::ToggleCodeActions { + deployed_from: Some(crate::actions::CodeActionSource::Indicator( + display_row, + )), + quick_launch: false, + }, + window, + cx, + ); + })) + .into_any_element() + } + + pub(super) fn refresh_code_actions_for_selection( + &mut self, + window: &mut Window, + cx: &mut Context, + ) { + self.code_actions_for_selection = CodeActionsForSelection::Fetching( + cx.spawn_in(window, async move |editor, cx| { + cx.background_executor() + .timer(CODE_ACTIONS_DEBOUNCE_TIMEOUT) + .await; + + let (start_buffer, start, _, end, _newest_selection) = editor + .update(cx, |editor, cx| { + let newest_selection = editor.selections.newest_anchor().clone(); + if newest_selection.head().diff_base_anchor().is_some() { + return None; + } + let display_snapshot = editor.display_snapshot(cx); + let newest_selection_adjusted = + editor.selections.newest_adjusted(&display_snapshot); + let buffer = editor.buffer.read(cx); + + let (start_buffer, start) = + buffer.text_anchor_for_position(newest_selection_adjusted.start, cx)?; + let (end_buffer, end) = + buffer.text_anchor_for_position(newest_selection_adjusted.end, cx)?; + + Some((start_buffer, start, end_buffer, end, newest_selection)) + }) + .ok() + .flatten() + .filter(|(start_buffer, _, end_buffer, _, _)| start_buffer == end_buffer)?; + + let (providers, tasks) = editor + .update_in(cx, |editor, window, cx| { + let providers = editor.code_action_providers.clone(); + let tasks = editor + .code_action_providers + .iter() + .map(|provider| { + provider.code_actions(&start_buffer, start..end, window, cx) + }) + .collect::>(); + (providers, tasks) + }) + .ok()?; + + let mut actions = Vec::new(); + for (provider, provider_actions) in + providers.into_iter().zip(future::join_all(tasks).await) + { + if let Some(provider_actions) = provider_actions.log_err() { + actions.extend(provider_actions.into_iter().map(|action| { + AvailableCodeAction { + action, + provider: provider.clone(), + } + })); + } + } + + editor + .update(cx, |editor, cx| { + let new_actions = if actions.is_empty() { + editor.code_actions_for_selection = CodeActionsForSelection::None; + None + } else { + let new_actions = ActionFetchReady { + location: Location { + buffer: start_buffer, + range: start..end, + }, + actions: Rc::from(actions), + }; + editor.code_actions_for_selection = + CodeActionsForSelection::Ready(new_actions.clone()); + Some(new_actions) + }; + cx.notify(); + new_actions + }) + .ok() + .flatten() + }) + .shared(), + ); + } + + fn debug_scenarios( + &mut self, + resolved_tasks: &Option, + buffer: &Entity, + cx: &mut App, + ) -> Task> { + maybe!({ + let project = self.project()?; + let dap_store = project.read(cx).dap_store(); + let mut scenarios = vec![]; + let resolved_tasks = resolved_tasks.as_ref()?; + let buffer = buffer.read(cx); + let language = buffer.language()?; + let debug_adapter = LanguageSettings::for_buffer(&buffer, cx) + .debuggers + .first() + .map(SharedString::from) + .or_else(|| language.config().debuggers.first().map(SharedString::from))?; + + dap_store.update(cx, |dap_store, cx| { + for (_, task) in &resolved_tasks.templates { + let maybe_scenario = dap_store.debug_scenario_for_build_task( + task.original_task().clone(), + debug_adapter.clone().into(), + task.display_label().to_owned().into(), + cx, + ); + scenarios.push(maybe_scenario); + } + }); + Some(cx.background_spawn(async move { + futures::future::join_all(scenarios) + .await + .into_iter() + .flatten() + .collect::>() + })) + }) + .unwrap_or_else(|| Task::ready(vec![])) + } +} + +pub trait CodeActionProvider { + fn id(&self) -> Arc; + + fn code_actions( + &self, + buffer: &Entity, + range: Range, + window: &mut Window, + cx: &mut App, + ) -> Task>>; + + fn apply_code_action( + &self, + buffer_handle: Entity, + action: CodeAction, + push_to_history: bool, + window: &mut Window, + cx: &mut App, + ) -> Task>; +} + +impl CodeActionProvider for Entity { + fn id(&self) -> Arc { + "project".into() + } + + fn code_actions( + &self, + buffer: &Entity, + range: Range, + _window: &mut Window, + cx: &mut App, + ) -> Task>> { + self.update(cx, |project, cx| { + let code_lens_actions = if EditorSettings::get_global(cx).code_lens.show_in_menu() { + Some(project.code_lens_actions(buffer, range.clone(), cx)) + } else { + None + }; + let code_actions = project.code_actions(buffer, range, None, cx); + cx.background_spawn(async move { + let code_lens_actions = match code_lens_actions { + Some(task) => task.await.context("code lens fetch")?.unwrap_or_default(), + None => Vec::new(), + }; + let code_actions = code_actions + .await + .context("code action fetch")? + .unwrap_or_default(); + Ok(code_lens_actions.into_iter().chain(code_actions).collect()) + }) + }) + } + + fn apply_code_action( + &self, + buffer_handle: Entity, + action: CodeAction, + push_to_history: bool, + _window: &mut Window, + cx: &mut App, + ) -> Task> { + self.update(cx, |project, cx| { + project.apply_code_action(buffer_handle, action, push_to_history, cx) + }) + } +} diff --git a/crates/editor/src/completions.rs b/crates/editor/src/completions.rs new file mode 100644 index 0000000000000000000000000000000000000000..2be7f28c5bf6fc9b6b7107fec122d4c25b1b3756 --- /dev/null +++ b/crates/editor/src/completions.rs @@ -0,0 +1,1489 @@ +use super::*; + +impl Editor { + pub fn set_completion_provider(&mut self, provider: Option>) { + self.completion_provider = provider; + } + + pub fn set_show_completions_on_input(&mut self, show_completions_on_input: Option) { + self.show_completions_on_input_override = show_completions_on_input; + } + + pub fn text_layout_details(&self, window: &mut Window, cx: &mut App) -> TextLayoutDetails { + TextLayoutDetails { + text_system: window.text_system().clone(), + editor_style: self.style.clone().unwrap_or_else(|| self.create_style(cx)), + rem_size: window.rem_size(), + scroll_anchor: self.scroll_manager.shared_scroll_anchor(cx), + visible_rows: self.visible_line_count(), + vertical_scroll_margin: self.scroll_manager.vertical_scroll_margin, + } + } + + pub fn show_word_completions( + &mut self, + _: &ShowWordCompletions, + window: &mut Window, + cx: &mut Context, + ) { + self.open_or_update_completions_menu( + Some(CompletionsMenuSource::Words { + ignore_threshold: true, + }), + None, + false, + window, + cx, + ); + } + + pub fn show_completions( + &mut self, + _: &ShowCompletions, + window: &mut Window, + cx: &mut Context, + ) { + self.open_or_update_completions_menu(None, None, false, window, cx); + } + + pub fn confirm_completion( + &mut self, + action: &ConfirmCompletion, + window: &mut Window, + cx: &mut Context, + ) -> Option>> { + if self.read_only(cx) { + return None; + } + self.do_completion(action.item_ix, CompletionIntent::Complete, window, cx) + } + + pub fn confirm_completion_insert( + &mut self, + _: &ConfirmCompletionInsert, + window: &mut Window, + cx: &mut Context, + ) -> Option>> { + if self.read_only(cx) { + return None; + } + self.do_completion(None, CompletionIntent::CompleteWithInsert, window, cx) + } + + pub fn confirm_completion_replace( + &mut self, + _: &ConfirmCompletionReplace, + window: &mut Window, + cx: &mut Context, + ) -> Option>> { + if self.read_only(cx) { + return None; + } + self.do_completion(None, CompletionIntent::CompleteWithReplace, window, cx) + } + + pub fn compose_completion( + &mut self, + action: &ComposeCompletion, + window: &mut Window, + cx: &mut Context, + ) -> Option>> { + self.do_completion(action.item_ix, CompletionIntent::Compose, window, cx) + } + + pub fn has_visible_completions_menu(&self) -> bool { + !self.edit_prediction_preview_is_active() + && self.context_menu.borrow().as_ref().is_some_and(|menu| { + menu.visible() && matches!(menu, CodeContextMenu::Completions(_)) + }) + } + + pub(super) fn trigger_completion_on_input( + &mut self, + text: &str, + trigger_in_words: bool, + window: &mut Window, + cx: &mut Context, + ) { + let completions_source = self + .context_menu + .borrow() + .as_ref() + .and_then(|menu| match menu { + CodeContextMenu::Completions(completions_menu) => Some(completions_menu.source), + CodeContextMenu::CodeActions(_) => None, + }); + + match completions_source { + Some(CompletionsMenuSource::Words { .. }) => { + self.open_or_update_completions_menu( + Some(CompletionsMenuSource::Words { + ignore_threshold: false, + }), + None, + trigger_in_words, + window, + cx, + ); + } + _ => self.open_or_update_completions_menu( + None, + Some(text.to_owned()).filter(|x| !x.is_empty()), + trigger_in_words, + window, + cx, + ), + } + } + + pub(super) fn is_lsp_relevant(&self, file: Option<&Arc>, cx: &App) -> bool { + let Some(project) = self.project() else { + return false; + }; + let Some(buffer_file) = project::File::from_dyn(file) else { + return false; + }; + let Some(entry_id) = buffer_file.project_entry_id() else { + return false; + }; + let project = project.read(cx); + let Some(buffer_worktree) = project.worktree_for_id(buffer_file.worktree_id(cx), cx) else { + return false; + }; + let Some(worktree_entry) = buffer_worktree.read(cx).entry_for_id(entry_id) else { + return false; + }; + !worktree_entry.is_ignored + } + + pub(super) fn visible_buffers(&self, cx: &mut Context) -> Vec> { + let display_snapshot = self.display_snapshot(cx); + let visible_range = self.multi_buffer_visible_range(&display_snapshot, cx); + let multi_buffer = self.buffer().read(cx); + display_snapshot + .buffer_snapshot() + .range_to_buffer_ranges(visible_range) + .into_iter() + .filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty()) + .filter_map(|(buffer_snapshot, _, _)| multi_buffer.buffer(buffer_snapshot.remote_id())) + .collect() + } + + pub(super) fn visible_buffer_ranges( + &self, + cx: &mut Context, + ) -> Vec<( + BufferSnapshot, + Range, + ExcerptRange, + )> { + let display_snapshot = self.display_snapshot(cx); + let visible_range = self.multi_buffer_visible_range(&display_snapshot, cx); + display_snapshot + .buffer_snapshot() + .range_to_buffer_ranges(visible_range) + .into_iter() + .filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty()) + .collect() + } + + pub(super) fn trigger_on_type_formatting( + &self, + input: String, + window: &mut Window, + cx: &mut Context, + ) -> Option>> { + if input.chars().count() != 1 { + return None; + } + + let project = self.project()?; + let position = self.selections.newest_anchor().head(); + let (buffer, buffer_position) = self + .buffer + .read(cx) + .text_anchor_for_position(position, cx)?; + + let settings = LanguageSettings::for_buffer_at(&buffer.read(cx), buffer_position, cx); + if !settings.use_on_type_format { + return None; + } + + // OnTypeFormatting returns a list of edits, no need to pass them between Zed instances, + // hence we do LSP request & edit on host side only — add formats to host's history. + let push_to_lsp_host_history = true; + // If this is not the host, append its history with new edits. + let push_to_client_history = project.read(cx).is_via_collab(); + + let on_type_formatting = project.update(cx, |project, cx| { + project.on_type_format( + buffer.clone(), + buffer_position, + input, + push_to_lsp_host_history, + cx, + ) + }); + Some(cx.spawn_in(window, async move |editor, cx| { + if let Some(transaction) = on_type_formatting.await? { + if push_to_client_history { + buffer.update(cx, |buffer, _| { + buffer.push_transaction(transaction, Instant::now()); + buffer.finalize_last_transaction(); + }); + } + editor.update(cx, |editor, cx| { + editor.refresh_document_highlights(cx); + })?; + } + Ok(()) + })) + } + + pub(super) fn open_or_update_completions_menu( + &mut self, + requested_source: Option, + trigger: Option, + trigger_in_words: bool, + window: &mut Window, + cx: &mut Context, + ) { + if self.pending_rename.is_some() { + return; + } + + let completions_source = self + .context_menu + .borrow() + .as_ref() + .and_then(|menu| match menu { + CodeContextMenu::Completions(completions_menu) => Some(completions_menu.source), + CodeContextMenu::CodeActions(_) => None, + }); + + 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 + .bias_right(&multibuffer_snapshot); + + if position.diff_base_anchor().is_some() { + return; + } + let multibuffer_position = multibuffer_snapshot.anchor_before(position); + let Some((buffer_position, _)) = + multibuffer_snapshot.anchor_to_buffer_anchor(multibuffer_position) + else { + return; + }; + let Some(buffer) = self.buffer.read(cx).buffer(buffer_position.buffer_id) else { + return; + }; + let buffer_snapshot = buffer.read(cx).snapshot(); + + let menu_is_open = matches!( + self.context_menu.borrow().as_ref(), + Some(CodeContextMenu::Completions(_)) + ); + + let language = buffer_snapshot + .language_at(buffer_position) + .map(|language| language.name()); + let language_settings = multibuffer_snapshot.language_settings_at(multibuffer_position, cx); + let completion_settings = language_settings.completions.clone(); + + let show_completions_on_input = self + .show_completions_on_input_override + .unwrap_or(language_settings.show_completions_on_input); + if !menu_is_open && trigger.is_some() && !show_completions_on_input { + return; + } + + let query: Option> = + Self::completion_query(&multibuffer_snapshot, multibuffer_position) + .map(|query| query.into()); + + drop(multibuffer_snapshot); + + // Hide the current completions menu when query is empty. Without this, cached + // completions from before the trigger char may be reused (#32774). + if query.is_none() && menu_is_open { + self.hide_context_menu(window, cx); + } + + let mut ignore_word_threshold = false; + let provider = match requested_source { + Some(CompletionsMenuSource::Normal) | None => self.completion_provider.clone(), + Some(CompletionsMenuSource::Words { ignore_threshold }) => { + ignore_word_threshold = ignore_threshold; + None + } + Some(CompletionsMenuSource::SnippetChoices) + | Some(CompletionsMenuSource::SnippetsOnly) => { + log::error!("bug: SnippetChoices requested_source is not handled"); + None + } + }; + + let sort_completions = provider + .as_ref() + .is_some_and(|provider| provider.sort_completions()); + + let filter_completions = provider + .as_ref() + .is_none_or(|provider| provider.filter_completions()); + + let was_snippets_only = matches!( + completions_source, + Some(CompletionsMenuSource::SnippetsOnly) + ); + + if let Some(CodeContextMenu::Completions(menu)) = self.context_menu.borrow_mut().as_mut() { + if filter_completions { + menu.filter( + query.clone().unwrap_or_default(), + buffer_position, + &buffer, + provider.clone(), + window, + cx, + ); + } + // When `is_incomplete` is false, no need to re-query completions when the current query + // is a suffix of the initial query. + let was_complete = !menu.is_incomplete; + if was_complete && !was_snippets_only { + // If the new query is a suffix of the old query (typing more characters) and + // the previous result was complete, the existing completions can be filtered. + // + // Note that snippet completions are always complete. + let query_matches = match (&menu.initial_query, &query) { + (Some(initial_query), Some(query)) => query.starts_with(initial_query.as_ref()), + (None, _) => true, + _ => false, + }; + if query_matches { + let position_matches = if menu.initial_position == position { + true + } else { + let snapshot = self.buffer.read(cx).read(cx); + menu.initial_position.to_offset(&snapshot) == position.to_offset(&snapshot) + }; + if position_matches { + return; + } + } + } + }; + + let (word_replace_range, word_to_exclude) = if let (word_range, Some(CharKind::Word)) = + buffer_snapshot.surrounding_word(buffer_position, None) + { + let word_to_exclude = buffer_snapshot + .text_for_range(word_range.clone()) + .collect::(); + ( + buffer_snapshot.anchor_before(word_range.start) + ..buffer_snapshot.anchor_after(buffer_position), + Some(word_to_exclude), + ) + } else { + (buffer_position..buffer_position, None) + }; + + let show_completion_documentation = buffer_snapshot + .settings_at(buffer_position, cx) + .show_completion_documentation; + + // The document can be large, so stay in reasonable bounds when searching for words, + // otherwise completion pop-up might be slow to appear. + const WORD_LOOKUP_ROWS: u32 = 5_000; + let buffer_row = text::ToPoint::to_point(&buffer_position, &buffer_snapshot).row; + let min_word_search = buffer_snapshot.clip_point( + Point::new(buffer_row.saturating_sub(WORD_LOOKUP_ROWS), 0), + Bias::Left, + ); + let max_word_search = buffer_snapshot.clip_point( + Point::new(buffer_row + WORD_LOOKUP_ROWS, 0).min(buffer_snapshot.max_point()), + Bias::Right, + ); + let word_search_range = buffer_snapshot.point_to_offset(min_word_search) + ..buffer_snapshot.point_to_offset(max_word_search); + + let skip_digits = query + .as_ref() + .is_none_or(|query| !query.chars().any(|c| c.is_digit(10))); + + let load_provider_completions = provider.as_ref().is_some_and(|provider| { + trigger.as_ref().is_none_or(|trigger| { + provider.is_completion_trigger( + &buffer, + buffer_position, + trigger, + trigger_in_words, + cx, + ) + }) + }); + + let provider_responses = if let Some(provider) = &provider + && load_provider_completions + { + let trigger_character = trigger + .as_ref() + .filter(|trigger| { + buffer + .read(cx) + .completion_triggers() + .contains(trigger.as_str()) + }) + .cloned(); + let completion_context = CompletionContext { + trigger_kind: match &trigger_character { + Some(_) => CompletionTriggerKind::TRIGGER_CHARACTER, + None => CompletionTriggerKind::INVOKED, + }, + trigger_character, + }; + + provider.completions(&buffer, buffer_position, completion_context, window, cx) + } else { + Task::ready(Ok(Vec::new())) + }; + + let load_word_completions = if !self.word_completions_enabled { + false + } else if requested_source + == Some(CompletionsMenuSource::Words { + ignore_threshold: true, + }) + { + true + } else { + load_provider_completions + && completion_settings.words != WordsCompletionMode::Disabled + && (ignore_word_threshold || { + let words_min_length = completion_settings.words_min_length; + // check whether word has at least `words_min_length` characters + let query_chars = query.iter().flat_map(|q| q.chars()); + query_chars.take(words_min_length).count() == words_min_length + }) + }; + + let mut words = if load_word_completions { + cx.background_spawn({ + let buffer_snapshot = buffer_snapshot.clone(); + async move { + buffer_snapshot.words_in_range(WordsQuery { + fuzzy_contents: None, + range: word_search_range, + skip_digits, + }) + } + }) + } else { + Task::ready(BTreeMap::default()) + }; + + let snippet_char_classifier = buffer_snapshot + .char_classifier_at(buffer_position) + .scope_context(Some(CharScopeContext::Completion)); + + let snippets = if let Some(provider) = &provider + && provider.show_snippets() + && let Some(project) = self.project() + { + let word_trigger = trigger.as_ref().is_some_and(|trigger| { + !trigger.is_empty() + && trigger + .chars() + .all(|character| snippet_char_classifier.is_word(character)) + }); + let requires_strong_snippet_match = !menu_is_open && !trigger_in_words && word_trigger; + let load_snippet_completions = !requires_strong_snippet_match + || query.as_ref().is_some_and(|query| { + let project = project.read(cx); + has_strong_snippet_prefix_match( + &project, + &buffer, + buffer_position, + &snippet_char_classifier, + query, + cx, + ) + }); + + if load_snippet_completions { + project.update(cx, |project, cx| { + snippet_completions( + project, + &buffer, + buffer_position, + snippet_char_classifier, + cx, + ) + }) + } else { + Task::ready(Ok(CompletionResponse { + completions: Vec::new(), + display_options: Default::default(), + is_incomplete: false, + })) + } + } else { + Task::ready(Ok(CompletionResponse { + completions: Vec::new(), + display_options: Default::default(), + is_incomplete: false, + })) + }; + + let snippet_sort_order = EditorSettings::get_global(cx).snippet_sort_order; + + let id = post_inc(&mut self.next_completion_id); + let task = cx.spawn_in(window, async move |editor, cx| { + let Ok(()) = editor.update(cx, |this, _| { + this.completion_tasks.retain(|(task_id, _)| *task_id >= id); + }) else { + return; + }; + + // TODO: Ideally completions from different sources would be selectively re-queried, so + // that having one source with `is_incomplete: true` doesn't cause all to be re-queried. + let mut completions = Vec::new(); + let mut is_incomplete = false; + let mut display_options: Option = None; + if let Some(provider_responses) = provider_responses.await.log_err() + && !provider_responses.is_empty() + { + for response in provider_responses { + completions.extend(response.completions); + is_incomplete = is_incomplete || response.is_incomplete; + match display_options.as_mut() { + None => { + display_options = Some(response.display_options); + } + Some(options) => options.merge(&response.display_options), + } + } + if completion_settings.words == WordsCompletionMode::Fallback { + words = Task::ready(BTreeMap::default()); + } + } + let display_options = display_options.unwrap_or_default(); + + let mut words = words.await; + if let Some(word_to_exclude) = &word_to_exclude { + words.remove(word_to_exclude); + } + for lsp_completion in &completions { + words.remove(&lsp_completion.new_text); + } + completions.extend(words.into_iter().map(|(word, word_range)| Completion { + replace_range: word_replace_range.clone(), + new_text: word.clone(), + label: CodeLabel::plain(word, None), + match_start: None, + snippet_deduplication_key: None, + icon_path: None, + documentation: None, + source: CompletionSource::BufferWord { + word_range, + resolved: false, + }, + insert_text_mode: Some(InsertTextMode::AS_IS), + confirm: None, + })); + + completions.extend( + snippets + .await + .into_iter() + .flat_map(|response| response.completions), + ); + + let menu = if completions.is_empty() { + None + } else { + let Ok((mut menu, matches_task)) = editor.update(cx, |editor, cx| { + let languages = editor + .workspace + .as_ref() + .and_then(|(workspace, _)| workspace.upgrade()) + .map(|workspace| workspace.read(cx).app_state().languages.clone()); + let menu = CompletionsMenu::new( + id, + requested_source.unwrap_or(if load_provider_completions { + CompletionsMenuSource::Normal + } else { + CompletionsMenuSource::SnippetsOnly + }), + sort_completions, + show_completion_documentation, + position, + query.clone(), + is_incomplete, + buffer.clone(), + completions.into(), + editor + .context_menu() + .borrow_mut() + .as_ref() + .map(|menu| menu.primary_scroll_handle()), + display_options, + snippet_sort_order, + languages, + language, + cx, + ); + + let query = if filter_completions { query } else { None }; + let matches_task = menu.do_async_filtering( + query.unwrap_or_default(), + buffer_position, + &buffer, + cx, + ); + (menu, matches_task) + }) else { + return; + }; + + let matches = matches_task.await; + + let Ok(()) = editor.update_in(cx, |editor, window, cx| { + // Newer menu already set, so exit. + if let Some(CodeContextMenu::Completions(prev_menu)) = + editor.context_menu.borrow().as_ref() + && prev_menu.id > id + { + return; + }; + + // Only valid to take prev_menu because either the new menu is immediately set + // below, or the menu is hidden. + if let Some(CodeContextMenu::Completions(prev_menu)) = + editor.context_menu.borrow_mut().take() + { + let position_matches = + if prev_menu.initial_position == menu.initial_position { + true + } else { + let snapshot = editor.buffer.read(cx).read(cx); + prev_menu.initial_position.to_offset(&snapshot) + == menu.initial_position.to_offset(&snapshot) + }; + if position_matches { + // Preserve markdown cache before `set_filter_results` because it will + // try to populate the documentation cache. + menu.preserve_markdown_cache(prev_menu); + } + }; + + menu.set_filter_results(matches, provider, window, cx); + }) else { + return; + }; + + menu.visible().then_some(menu) + }; + + editor + .update_in(cx, |editor, window, cx| { + if editor.focus_handle.is_focused(window) + && let Some(menu) = menu + { + *editor.context_menu.borrow_mut() = + Some(CodeContextMenu::Completions(menu)); + + crate::hover_popover::hide_hover(editor, cx); + if editor.show_edit_predictions_in_menu() { + editor.update_visible_edit_prediction(window, cx); + } else { + editor + .discard_edit_prediction(EditPredictionDiscardReason::Ignored, cx); + } + + cx.notify(); + return; + } + + if editor.completion_tasks.len() <= 1 { + // If there are no more completion tasks and the last menu was empty, we should hide it. + let was_hidden = editor.hide_context_menu(window, cx).is_none(); + // If it was already hidden and we don't show edit predictions in the menu, + // we should also show the edit prediction when available. + if was_hidden && editor.show_edit_predictions_in_menu() { + editor.update_visible_edit_prediction(window, cx); + } + } + }) + .ok(); + }); + + self.completion_tasks.push((id, task)); + } + + pub(super) fn with_completions_menu_matching_id( + &self, + id: CompletionId, + f: impl FnOnce(Option<&mut CompletionsMenu>) -> R, + ) -> R { + let mut context_menu = self.context_menu.borrow_mut(); + let Some(CodeContextMenu::Completions(completions_menu)) = &mut *context_menu else { + return f(None); + }; + if completions_menu.id != id { + return f(None); + } + f(Some(completions_menu)) + } + + fn completion_query(buffer: &MultiBufferSnapshot, position: impl ToOffset) -> Option { + let offset = position.to_offset(buffer); + let (word_range, kind) = + buffer.surrounding_word(offset, Some(CharScopeContext::Completion)); + if offset > word_range.start && kind == Some(CharKind::Word) { + Some( + buffer + .text_for_range(word_range.start..offset) + .collect::(), + ) + } else { + None + } + } + + fn do_completion( + &mut self, + item_ix: Option, + intent: CompletionIntent, + window: &mut Window, + cx: &mut Context, + ) -> Option>> { + use language::ToOffset as _; + + let CodeContextMenu::Completions(completions_menu) = self.hide_context_menu(window, cx)? + else { + return None; + }; + + let candidate_id = { + let entries = completions_menu.entries.borrow(); + let mat = entries.get(item_ix.unwrap_or(completions_menu.selected_item))?; + if self.show_edit_predictions_in_menu() { + self.discard_edit_prediction(EditPredictionDiscardReason::Rejected, cx); + } + mat.candidate_id + }; + + let completion = completions_menu + .completions + .borrow() + .get(candidate_id)? + .clone(); + cx.stop_propagation(); + + let buffer_handle = completions_menu.buffer.clone(); + let multibuffer_snapshot = self.buffer.read(cx).snapshot(cx); + let (initial_position, _) = + multibuffer_snapshot.anchor_to_buffer_anchor(completions_menu.initial_position)?; + + let CompletionEdit { + new_text, + snippet, + replace_range, + } = process_completion_for_edit(&completion, intent, &buffer_handle, &initial_position, cx); + + let buffer = buffer_handle.read(cx).snapshot(); + let newest_selection = self.selections.newest_anchor(); + + let Some(replace_range_multibuffer) = + multibuffer_snapshot.buffer_anchor_range_to_anchor_range(replace_range.clone()) + else { + return None; + }; + + let Some((buffer_snapshot, newest_range_buffer)) = + multibuffer_snapshot.anchor_range_to_buffer_anchor_range(newest_selection.range()) + else { + return None; + }; + + let old_text = buffer + .text_for_range(replace_range.clone()) + .collect::(); + let lookbehind = newest_range_buffer + .start + .to_offset(buffer_snapshot) + .saturating_sub(replace_range.start.to_offset(&buffer_snapshot)); + let lookahead = replace_range + .end + .to_offset(&buffer_snapshot) + .saturating_sub(newest_range_buffer.end.to_offset(&buffer)); + let prefix = &old_text[..old_text.len().saturating_sub(lookahead)]; + let suffix = &old_text[lookbehind.min(old_text.len())..]; + + let selections = self + .selections + .all::(&self.display_snapshot(cx)); + let mut ranges = Vec::new(); + let mut all_commit_ranges = Vec::new(); + let mut linked_edits = LinkedEdits::new(); + + let text: Arc = new_text.clone().into(); + for selection in &selections { + let range = if selection.id == newest_selection.id { + replace_range_multibuffer.clone() + } else { + let mut range = selection.range(); + + // if prefix is present, don't duplicate it + if multibuffer_snapshot + .contains_str_at(range.start.saturating_sub_usize(lookbehind), prefix) + { + range.start = range.start.saturating_sub_usize(lookbehind); + + // if suffix is also present, mimic the newest cursor and replace it + if selection.id != newest_selection.id + && multibuffer_snapshot.contains_str_at(range.end, suffix) + { + range.end += lookahead; + } + } + range.to_anchors(&multibuffer_snapshot) + }; + + ranges.push(range.clone()); + + let start_anchor = multibuffer_snapshot.anchor_before(range.start); + let end_anchor = multibuffer_snapshot.anchor_after(range.end); + + if let Some((buffer_snapshot_2, anchor_range)) = + multibuffer_snapshot.anchor_range_to_buffer_anchor_range(start_anchor..end_anchor) + && buffer_snapshot_2.remote_id() == buffer_snapshot.remote_id() + { + all_commit_ranges.push(anchor_range.clone()); + if !self.linked_edit_ranges.is_empty() { + linked_edits.push(&self, anchor_range, text.clone(), cx); + } + } + } + + let common_prefix_len = old_text + .chars() + .zip(new_text.chars()) + .take_while(|(a, b)| a == b) + .map(|(a, _)| a.len_utf8()) + .sum::(); + + cx.emit(EditorEvent::InputHandled { + utf16_range_to_replace: None, + text: new_text[common_prefix_len..].into(), + }); + + let tx_id = self.transact(window, cx, |editor, window, cx| { + if let Some(mut snippet) = snippet { + snippet.text = new_text.to_string(); + let offset_ranges = ranges + .iter() + .map(|range| range.to_offset(&multibuffer_snapshot)) + .collect::>(); + editor + .insert_snippet(&offset_ranges, snippet, window, cx) + .log_err(); + } else { + editor.buffer.update(cx, |multi_buffer, cx| { + let auto_indent = match completion.insert_text_mode { + Some(InsertTextMode::AS_IS) => None, + _ => editor.autoindent_mode.clone(), + }; + let edits = ranges.into_iter().map(|range| (range, new_text.as_str())); + multi_buffer.edit(edits, auto_indent, cx); + }); + } + linked_edits.apply(cx); + editor.refresh_edit_prediction(true, false, window, cx); + }); + self.invalidate_autoclose_regions( + &self.selections.disjoint_anchors_arc(), + &multibuffer_snapshot, + ); + + let show_new_completions_on_confirm = completion + .confirm + .as_ref() + .is_some_and(|confirm| confirm(intent, window, cx)); + if show_new_completions_on_confirm { + self.open_or_update_completions_menu(None, None, false, window, cx); + } + + let provider = self.completion_provider.as_ref()?; + + let lsp_store = self.project().map(|project| project.read(cx).lsp_store()); + let command = lsp_store.as_ref().and_then(|lsp_store| { + let CompletionSource::Lsp { + lsp_completion, + server_id, + .. + } = &completion.source + else { + return None; + }; + let lsp_command = lsp_completion.command.as_ref()?; + let available_commands = lsp_store + .read(cx) + .lsp_server_capabilities + .get(server_id) + .and_then(|server_capabilities| { + server_capabilities + .execute_command_provider + .as_ref() + .map(|options| options.commands.as_slice()) + })?; + if available_commands.contains(&lsp_command.command) { + Some(CodeAction { + server_id: *server_id, + range: language::Anchor::min_min_range_for_buffer(buffer.remote_id()), + lsp_action: LspAction::Command(lsp_command.clone()), + resolved: false, + }) + } else { + None + } + }); + + drop(completion); + let apply_edits = provider.apply_additional_edits_for_completion( + buffer_handle.clone(), + completions_menu.completions.clone(), + candidate_id, + true, + all_commit_ranges, + cx, + ); + + let editor_settings = EditorSettings::get_global(cx); + if editor_settings.show_signature_help_after_edits || editor_settings.auto_signature_help { + // After the code completion is finished, users often want to know what signatures are needed. + // so we should automatically call signature_help + self.show_signature_help(&ShowSignatureHelp, window, cx); + } + + Some(cx.spawn_in(window, async move |editor, cx| { + let additional_edits_tx = apply_edits.await?; + + if let Some((lsp_store, command)) = lsp_store.zip(command) { + let title = command.lsp_action.title().to_owned(); + let project_transaction = lsp_store + .update(cx, |lsp_store, cx| { + lsp_store.apply_code_action(buffer_handle, command, false, cx) + }) + .await + .context("applying post-completion command")?; + if let Some(workspace) = editor.read_with(cx, |editor, _| editor.workspace())? { + Self::open_project_transaction( + &editor, + workspace.downgrade(), + project_transaction, + title, + cx, + ) + .await?; + } + } + + if let Some(tx_id) = tx_id + && let Some(additional_edits_tx) = additional_edits_tx + { + editor + .update(cx, |editor, cx| { + editor.buffer.update(cx, |buffer, cx| { + buffer.merge_transactions(additional_edits_tx.id, tx_id, cx) + }); + }) + .context("merge transactions")?; + } + + Ok(()) + })) + } +} + +#[cfg(any(test, feature = "test-support"))] +impl Editor { + pub fn completion_provider(&self) -> Option> { + self.completion_provider.clone() + } + + pub fn current_completions(&self) -> Option> { + let menu = self.context_menu.borrow(); + if let CodeContextMenu::Completions(menu) = menu.as_ref()? { + let completions = menu.completions.borrow(); + Some(completions.to_vec()) + } else { + None + } + } + + #[cfg(test)] + pub(super) fn disable_word_completions(&mut self) { + self.word_completions_enabled = false; + } +} + +pub trait CompletionProvider { + fn completions( + &self, + buffer: &Entity, + buffer_position: text::Anchor, + trigger: CompletionContext, + window: &mut Window, + cx: &mut Context, + ) -> Task>>; + + fn resolve_completions( + &self, + _buffer: Entity, + _completion_indices: Vec, + _completions: Rc>>, + _cx: &mut Context, + ) -> Task> { + Task::ready(Ok(false)) + } + + fn apply_additional_edits_for_completion( + &self, + _buffer: Entity, + _completions: Rc>>, + _completion_index: usize, + _push_to_history: bool, + _all_commit_ranges: Vec>, + _cx: &mut Context, + ) -> Task>> { + Task::ready(Ok(None)) + } + + fn is_completion_trigger( + &self, + buffer: &Entity, + position: language::Anchor, + text: &str, + trigger_in_words: bool, + cx: &mut Context, + ) -> bool; + + fn selection_changed(&self, _mat: Option<&StringMatch>, _window: &mut Window, _cx: &mut App) {} + + fn sort_completions(&self) -> bool { + true + } + + fn filter_completions(&self) -> bool { + true + } + + fn show_snippets(&self) -> bool { + false + } +} + +fn has_strong_snippet_prefix_match( + project: &Project, + buffer: &Entity, + buffer_anchor: text::Anchor, + classifier: &CharClassifier, + query: &str, + cx: &App, +) -> bool { + if query.chars().take(2).count() < 2 { + return false; + } + + let query = query.to_lowercase(); + let is_word_char = |character| classifier.is_word(character); + let languages = buffer.read(cx).languages_at(buffer_anchor); + let snippet_store = project.snippets().read(cx); + + languages.iter().any(|language| { + snippet_store + .snippets_for(Some(language.lsp_id()), cx) + .iter() + .flat_map(|snippet| snippet.prefix.iter()) + .flat_map(|prefix| snippet_candidate_suffixes(prefix, &is_word_char)) + .any(|candidate| candidate.to_lowercase().starts_with(&query)) + }) +} + +fn snippet_completions( + project: &Project, + buffer: &Entity, + buffer_anchor: text::Anchor, + classifier: CharClassifier, + cx: &mut App, +) -> Task> { + let languages = buffer.read(cx).languages_at(buffer_anchor); + let snippet_store = project.snippets().read(cx); + + let scopes: Vec<_> = languages + .iter() + .filter_map(|language| { + let language_name = language.lsp_id(); + let snippets = snippet_store.snippets_for(Some(language_name), cx); + + if snippets.is_empty() { + None + } else { + Some((language.default_scope(), snippets)) + } + }) + .collect(); + + if scopes.is_empty() { + return Task::ready(Ok(CompletionResponse { + completions: vec![], + display_options: CompletionDisplayOptions::default(), + is_incomplete: false, + })); + } + + let snapshot = buffer.read(cx).text_snapshot(); + let executor = cx.background_executor().clone(); + + cx.background_spawn(async move { + let is_word_char = |c| classifier.is_word(c); + + let mut is_incomplete = false; + let mut completions: Vec = Vec::new(); + + const MAX_PREFIX_LEN: usize = 128; + let buffer_offset = text::ToOffset::to_offset(&buffer_anchor, &snapshot); + let window_start = buffer_offset.saturating_sub(MAX_PREFIX_LEN); + let window_start = snapshot.clip_offset(window_start, Bias::Left); + + let max_buffer_window: String = snapshot + .text_for_range(window_start..buffer_offset) + .collect(); + + if max_buffer_window.is_empty() { + return Ok(CompletionResponse { + completions: vec![], + display_options: CompletionDisplayOptions::default(), + is_incomplete: true, + }); + } + + for (_scope, snippets) in scopes.into_iter() { + // Sort snippets by word count to match longer snippet prefixes first. + let mut sorted_snippet_candidates = snippets + .iter() + .enumerate() + .flat_map(|(snippet_ix, snippet)| { + snippet + .prefix + .iter() + .enumerate() + .map(move |(prefix_ix, prefix)| { + let word_count = + snippet_candidate_suffixes(prefix, &is_word_char).count(); + ((snippet_ix, prefix_ix), prefix, word_count) + }) + }) + .collect_vec(); + sorted_snippet_candidates + .sort_unstable_by_key(|(_, _, word_count)| Reverse(*word_count)); + + // Each prefix may be matched multiple times; the completion menu must filter out duplicates. + + let buffer_windows = snippet_candidate_suffixes(&max_buffer_window, &is_word_char) + .take( + sorted_snippet_candidates + .first() + .map(|(_, _, word_count)| *word_count) + .unwrap_or_default(), + ) + .collect_vec(); + + const MAX_RESULTS: usize = 100; + // Each match also remembers how many characters from the buffer it consumed + let mut matches: Vec<(StringMatch, usize)> = vec![]; + + let mut snippet_list_cutoff_index = 0; + for (buffer_index, buffer_window) in buffer_windows.iter().enumerate().rev() { + let word_count = buffer_index + 1; + // Increase `snippet_list_cutoff_index` until we have all of the + // snippets with sufficiently many words. + while sorted_snippet_candidates + .get(snippet_list_cutoff_index) + .is_some_and(|(_ix, _prefix, snippet_word_count)| { + *snippet_word_count >= word_count + }) + { + snippet_list_cutoff_index += 1; + } + + // Take only the candidates with at least `word_count` many words + let snippet_candidates_at_word_len = + &sorted_snippet_candidates[..snippet_list_cutoff_index]; + + let candidates = snippet_candidates_at_word_len + .iter() + .map(|(_snippet_ix, prefix, _snippet_word_count)| prefix) + .enumerate() // index in `sorted_snippet_candidates` + // First char must match + .filter(|(_ix, prefix)| { + itertools::equal( + prefix + .chars() + .next() + .into_iter() + .flat_map(|c| c.to_lowercase()), + buffer_window + .chars() + .next() + .into_iter() + .flat_map(|c| c.to_lowercase()), + ) + }) + .map(|(ix, prefix)| StringMatchCandidate::new(ix, prefix)) + .collect::>(); + + matches.extend( + fuzzy::match_strings( + &candidates, + &buffer_window, + buffer_window.chars().any(|c| c.is_uppercase()), + true, + MAX_RESULTS - matches.len(), // always prioritize longer snippets + &Default::default(), + executor.clone(), + ) + .await + .into_iter() + .map(|string_match| (string_match, buffer_window.len())), + ); + + if matches.len() >= MAX_RESULTS { + break; + } + } + + let to_lsp = |point: &text::Anchor| { + let end = text::ToPointUtf16::to_point_utf16(point, &snapshot); + point_to_lsp(end) + }; + let lsp_end = to_lsp(&buffer_anchor); + + if matches.len() >= MAX_RESULTS { + is_incomplete = true; + } + + completions.extend(matches.iter().map(|(string_match, buffer_window_len)| { + let ((snippet_index, prefix_index), matching_prefix, _snippet_word_count) = + sorted_snippet_candidates[string_match.candidate_id]; + let snippet = &snippets[snippet_index]; + let start = buffer_offset - buffer_window_len; + let start = snapshot.anchor_before(start); + let range = start..buffer_anchor; + let lsp_start = to_lsp(&start); + let lsp_range = lsp::Range { + start: lsp_start, + end: lsp_end, + }; + Completion { + replace_range: range, + new_text: snippet.body.clone(), + source: CompletionSource::Lsp { + insert_range: None, + server_id: LanguageServerId(usize::MAX), + resolved: true, + lsp_completion: Box::new(lsp::CompletionItem { + label: matching_prefix.clone(), + kind: Some(CompletionItemKind::SNIPPET), + label_details: snippet.description.as_ref().map(|description| { + lsp::CompletionItemLabelDetails { + detail: Some(description.clone()), + description: None, + } + }), + insert_text_format: Some(InsertTextFormat::SNIPPET), + text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace( + lsp::InsertReplaceEdit { + new_text: snippet.body.clone(), + insert: lsp_range, + replace: lsp_range, + }, + )), + filter_text: Some(snippet.body.clone()), + sort_text: Some(char::MAX.to_string()), + ..lsp::CompletionItem::default() + }), + lsp_defaults: None, + }, + label: CodeLabel { + text: matching_prefix.clone(), + runs: Vec::new(), + filter_range: 0..matching_prefix.len(), + }, + icon_path: None, + documentation: Some(CompletionDocumentation::SingleLineAndMultiLinePlainText { + single_line: snippet.name.clone().into(), + plain_text: snippet + .description + .clone() + .map(|description| description.into()), + }), + insert_text_mode: None, + confirm: None, + match_start: Some(start), + snippet_deduplication_key: Some((snippet_index, prefix_index)), + } + })); + } + + Ok(CompletionResponse { + completions, + display_options: CompletionDisplayOptions::default(), + is_incomplete, + }) + }) +} + +impl CompletionProvider for Entity { + fn completions( + &self, + buffer: &Entity, + buffer_position: text::Anchor, + options: CompletionContext, + _window: &mut Window, + cx: &mut Context, + ) -> Task>> { + self.update(cx, |project, cx| { + let task = project.completions(buffer, buffer_position, options, cx); + cx.background_spawn(task) + }) + } + + fn resolve_completions( + &self, + buffer: Entity, + completion_indices: Vec, + completions: Rc>>, + cx: &mut Context, + ) -> Task> { + self.update(cx, |project, cx| { + project.lsp_store().update(cx, |lsp_store, cx| { + lsp_store.resolve_completions(buffer, completion_indices, completions, cx) + }) + }) + } + + fn apply_additional_edits_for_completion( + &self, + buffer: Entity, + completions: Rc>>, + completion_index: usize, + push_to_history: bool, + all_commit_ranges: Vec>, + cx: &mut Context, + ) -> Task>> { + self.update(cx, |project, cx| { + project.lsp_store().update(cx, |lsp_store, cx| { + lsp_store.apply_additional_edits_for_completion( + buffer, + completions, + completion_index, + push_to_history, + all_commit_ranges, + cx, + ) + }) + }) + } + + fn is_completion_trigger( + &self, + buffer: &Entity, + position: language::Anchor, + text: &str, + trigger_in_words: bool, + cx: &mut Context, + ) -> bool { + let mut chars = text.chars(); + let char = if let Some(char) = chars.next() { + char + } else { + return false; + }; + if chars.next().is_some() { + return false; + } + + let buffer = buffer.read(cx); + let snapshot = buffer.snapshot(); + let classifier = snapshot + .char_classifier_at(position) + .scope_context(Some(CharScopeContext::Completion)); + if trigger_in_words && classifier.is_word(char) { + return true; + } + + buffer.completion_triggers().contains(text) + } + + fn show_snippets(&self) -> bool { + true + } +} + +pub(crate) fn split_words(text: &str) -> impl std::iter::Iterator + '_ { + let mut prev_index = 0; + let mut prev_codepoint: Option = None; + text.char_indices() + .chain([(text.len(), '\0')]) + .filter_map(move |(index, codepoint)| { + let prev_codepoint = prev_codepoint.replace(codepoint)?; + let is_boundary = index == text.len() + || !prev_codepoint.is_uppercase() && codepoint.is_uppercase() + || !prev_codepoint.is_alphanumeric() && codepoint.is_alphanumeric(); + if is_boundary { + let chunk = &text[prev_index..index]; + prev_index = index; + Some(chunk) + } else { + None + } + }) +} + +/// Given a string of text immediately before the cursor, iterates over possible +/// strings a snippet could match to. More precisely: returns an iterator over +/// suffixes of `text` created by splitting at word boundaries (before & after +/// every non-word character). +/// +/// Shorter suffixes are returned first. +pub(crate) fn snippet_candidate_suffixes<'a>( + text: &'a str, + is_word_char: &'a dyn Fn(char) -> bool, +) -> impl std::iter::Iterator + 'a { + let mut prev_index = text.len(); + let mut prev_codepoint = None; + text.char_indices() + .rev() + .chain([(0, '\0')]) + .filter_map(move |(index, codepoint)| { + let prev_index = std::mem::replace(&mut prev_index, index); + let prev_codepoint = prev_codepoint.replace(codepoint)?; + if is_word_char(prev_codepoint) && is_word_char(codepoint) { + None + } else { + let chunk = &text[prev_index..]; // go to end of string + Some(chunk) + } + }) +} diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6dcc10b0ee22956811b36e87f554dfb9b9b8014f..175b430ff014cfbb2725c1404c5e22589856c682 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -57,11 +57,18 @@ mod signature_help; #[cfg(any(test, feature = "test-support"))] pub mod test; +mod code_actions; +mod completions; mod config; mod diagnostics; mod rewrap; pub(crate) use actions::*; +pub use code_actions::CodeActionProvider; +pub use completions::CompletionProvider; +#[cfg(test)] +pub(crate) use completions::snippet_candidate_suffixes; +pub(crate) use completions::split_words; use diagnostics::{ActiveDiagnostic, GlobalDiagnosticRenderer, InlineDiagnostic}; pub use diagnostics::{DiagnosticRenderer, set_diagnostic_renderer}; pub use display_map::{ @@ -3362,15 +3369,6 @@ impl Editor { self.custom_context_menu = Some(Box::new(f)) } - pub fn set_completion_provider(&mut self, provider: Option>) { - self.completion_provider = provider; - } - - #[cfg(any(test, feature = "test-support"))] - pub fn completion_provider(&self) -> Option> { - self.completion_provider.clone() - } - pub fn semantics_provider(&self) -> Option> { self.semantics_provider.clone() } @@ -3578,10 +3576,6 @@ impl Editor { } } - pub fn set_show_completions_on_input(&mut self, show_completions_on_input: Option) { - self.show_completions_on_input_override = show_completions_on_input; - } - pub fn set_show_edit_predictions( &mut self, show_edit_predictions: Option, @@ -5748,44 +5742,6 @@ impl Editor { Some(()) } - fn trigger_completion_on_input( - &mut self, - text: &str, - trigger_in_words: bool, - window: &mut Window, - cx: &mut Context, - ) { - let completions_source = self - .context_menu - .borrow() - .as_ref() - .and_then(|menu| match menu { - CodeContextMenu::Completions(completions_menu) => Some(completions_menu.source), - CodeContextMenu::CodeActions(_) => None, - }); - - match completions_source { - Some(CompletionsMenuSource::Words { .. }) => { - self.open_or_update_completions_menu( - Some(CompletionsMenuSource::Words { - ignore_threshold: false, - }), - None, - trigger_in_words, - window, - cx, - ); - } - _ => self.open_or_update_completions_menu( - None, - Some(text.to_owned()).filter(|x| !x.is_empty()), - trigger_in_words, - window, - cx, - ), - } - } - /// If any empty selections is touching the start of its innermost containing autoclose /// region, expand it to select the brackets. fn select_autoclose_pair(&mut self, window: &mut Window, cx: &mut Context) { @@ -5917,1371 +5873,98 @@ impl Editor { }); } - fn completion_query(buffer: &MultiBufferSnapshot, position: impl ToOffset) -> Option { - let offset = position.to_offset(buffer); - let (word_range, kind) = - buffer.surrounding_word(offset, Some(CharScopeContext::Completion)); - if offset > word_range.start && kind == Some(CharKind::Word) { - Some( - buffer - .text_for_range(word_range.start..offset) - .collect::(), - ) - } else { - None - } - } - - pub fn is_lsp_relevant(&self, file: Option<&Arc>, cx: &App) -> bool { - let Some(project) = self.project() else { - return false; - }; - let Some(buffer_file) = project::File::from_dyn(file) else { - return false; - }; - let Some(entry_id) = buffer_file.project_entry_id() else { - return false; - }; - let project = project.read(cx); - let Some(buffer_worktree) = project.worktree_for_id(buffer_file.worktree_id(cx), cx) else { - return false; - }; - let Some(worktree_entry) = buffer_worktree.read(cx).entry_for_id(entry_id) else { - return false; - }; - !worktree_entry.is_ignored - } - - pub fn visible_buffers(&self, cx: &mut Context) -> Vec> { - let display_snapshot = self.display_snapshot(cx); - let visible_range = self.multi_buffer_visible_range(&display_snapshot, cx); - let multi_buffer = self.buffer().read(cx); - display_snapshot - .buffer_snapshot() - .range_to_buffer_ranges(visible_range) - .into_iter() - .filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty()) - .filter_map(|(buffer_snapshot, _, _)| multi_buffer.buffer(buffer_snapshot.remote_id())) - .collect() - } - - pub fn visible_buffer_ranges( - &self, - cx: &mut Context, - ) -> Vec<( - BufferSnapshot, - Range, - ExcerptRange, - )> { - let display_snapshot = self.display_snapshot(cx); - let visible_range = self.multi_buffer_visible_range(&display_snapshot, cx); - display_snapshot - .buffer_snapshot() - .range_to_buffer_ranges(visible_range) - .into_iter() - .filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty()) - .collect() - } - - pub fn text_layout_details(&self, window: &mut Window, cx: &mut App) -> TextLayoutDetails { - TextLayoutDetails { - text_system: window.text_system().clone(), - editor_style: self.style.clone().unwrap_or_else(|| self.create_style(cx)), - rem_size: window.rem_size(), - scroll_anchor: self.scroll_manager.shared_scroll_anchor(cx), - visible_rows: self.visible_line_count(), - vertical_scroll_margin: self.scroll_manager.vertical_scroll_margin, - } - } - - fn trigger_on_type_formatting( - &self, - input: String, - window: &mut Window, - cx: &mut Context, - ) -> Option>> { - if input.chars().count() != 1 { - return None; - } - - let project = self.project()?; - let position = self.selections.newest_anchor().head(); - let (buffer, buffer_position) = self - .buffer - .read(cx) - .text_anchor_for_position(position, cx)?; - - let settings = LanguageSettings::for_buffer_at(&buffer.read(cx), buffer_position, cx); - if !settings.use_on_type_format { - return None; - } - - // OnTypeFormatting returns a list of edits, no need to pass them between Zed instances, - // hence we do LSP request & edit on host side only — add formats to host's history. - let push_to_lsp_host_history = true; - // If this is not the host, append its history with new edits. - let push_to_client_history = project.read(cx).is_via_collab(); - - let on_type_formatting = project.update(cx, |project, cx| { - project.on_type_format( - buffer.clone(), - buffer_position, - input, - push_to_lsp_host_history, - cx, - ) - }); - Some(cx.spawn_in(window, async move |editor, cx| { - if let Some(transaction) = on_type_formatting.await? { - if push_to_client_history { - buffer.update(cx, |buffer, _| { - buffer.push_transaction(transaction, Instant::now()); - buffer.finalize_last_transaction(); - }); - } - editor.update(cx, |editor, cx| { - editor.refresh_document_highlights(cx); - })?; - } - Ok(()) - })) - } - - pub fn show_word_completions( - &mut self, - _: &ShowWordCompletions, - window: &mut Window, - cx: &mut Context, - ) { - self.open_or_update_completions_menu( - Some(CompletionsMenuSource::Words { - ignore_threshold: true, - }), - None, - false, - window, - cx, - ); - } - - pub fn show_completions( - &mut self, - _: &ShowCompletions, - window: &mut Window, - cx: &mut Context, - ) { - self.open_or_update_completions_menu(None, None, false, window, cx); - } - - fn open_or_update_completions_menu( - &mut self, - requested_source: Option, - trigger: Option, - trigger_in_words: bool, + fn open_transaction_for_hidden_buffers( + workspace: Entity, + transaction: ProjectTransaction, + title: String, window: &mut Window, cx: &mut Context, ) { - if self.pending_rename.is_some() { + if transaction.0.is_empty() { return; } - let completions_source = self - .context_menu - .borrow() - .as_ref() - .and_then(|menu| match menu { - CodeContextMenu::Completions(completions_menu) => Some(completions_menu.source), - CodeContextMenu::CodeActions(_) => None, - }); - - 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 - .bias_right(&multibuffer_snapshot); + let edited_buffers_already_open = { + let other_editors: Vec> = workspace + .read(cx) + .panes() + .iter() + .flat_map(|pane| pane.read(cx).items_of_type::()) + .filter(|editor| editor.entity_id() != cx.entity_id()) + .collect(); - if position.diff_base_anchor().is_some() { - return; - } - let multibuffer_position = multibuffer_snapshot.anchor_before(position); - let Some((buffer_position, _)) = - multibuffer_snapshot.anchor_to_buffer_anchor(multibuffer_position) - else { - return; - }; - let Some(buffer) = self.buffer.read(cx).buffer(buffer_position.buffer_id) else { - return; + transaction.0.keys().all(|buffer| { + other_editors.iter().any(|editor| { + let multi_buffer = editor.read(cx).buffer(); + multi_buffer.read(cx).is_singleton() + && multi_buffer + .read(cx) + .as_singleton() + .map_or(false, |singleton| { + singleton.entity_id() == buffer.entity_id() + }) + }) + }) }; - let buffer_snapshot = buffer.read(cx).snapshot(); - - let menu_is_open = matches!( - self.context_menu.borrow().as_ref(), - Some(CodeContextMenu::Completions(_)) - ); - - let language = buffer_snapshot - .language_at(buffer_position) - .map(|language| language.name()); - let language_settings = multibuffer_snapshot.language_settings_at(multibuffer_position, cx); - let completion_settings = language_settings.completions.clone(); - - let show_completions_on_input = self - .show_completions_on_input_override - .unwrap_or(language_settings.show_completions_on_input); - if !menu_is_open && trigger.is_some() && !show_completions_on_input { - return; + if !edited_buffers_already_open { + let workspace = workspace.downgrade(); + cx.defer_in(window, move |_, window, cx| { + cx.spawn_in(window, async move |editor, cx| { + Self::open_project_transaction(&editor, workspace, transaction, title, cx) + .await + .ok() + }) + .detach(); + }); } + } - let query: Option> = - Self::completion_query(&multibuffer_snapshot, multibuffer_position) - .map(|query| query.into()); - - drop(multibuffer_snapshot); - - // Hide the current completions menu when query is empty. Without this, cached - // completions from before the trigger char may be reused (#32774). - if query.is_none() && menu_is_open { - self.hide_context_menu(window, cx); + pub async fn open_project_transaction( + editor: &WeakEntity, + workspace: WeakEntity, + transaction: ProjectTransaction, + title: String, + cx: &mut AsyncWindowContext, + ) -> Result<()> { + let mut entries = transaction.0.into_iter().collect::>(); + cx.update(|_, cx| { + entries.sort_unstable_by_key(|(buffer, _)| { + buffer.read(cx).file().map(|f| f.path().clone()) + }); + })?; + if entries.is_empty() { + return Ok(()); } - let mut ignore_word_threshold = false; - let provider = match requested_source { - Some(CompletionsMenuSource::Normal) | None => self.completion_provider.clone(), - Some(CompletionsMenuSource::Words { ignore_threshold }) => { - ignore_word_threshold = ignore_threshold; - None - } - Some(CompletionsMenuSource::SnippetChoices) - | Some(CompletionsMenuSource::SnippetsOnly) => { - log::error!("bug: SnippetChoices requested_source is not handled"); - None - } - }; - - let sort_completions = provider - .as_ref() - .is_some_and(|provider| provider.sort_completions()); + // If the project transaction's edits are all contained within this editor, then + // avoid opening a new editor to display them. - let filter_completions = provider - .as_ref() - .is_none_or(|provider| provider.filter_completions()); + if let [(buffer, transaction)] = &*entries { + let cursor_excerpt = editor.update(cx, |editor, cx| { + let snapshot = editor.buffer().read(cx).snapshot(cx); + let head = editor.selections.newest_anchor().head(); + let (buffer_snapshot, excerpt_range) = snapshot.excerpt_containing(head..head)?; + if buffer_snapshot.remote_id() != buffer.read(cx).remote_id() { + return None; + } + Some(excerpt_range) + })?; - let was_snippets_only = matches!( - completions_source, - Some(CompletionsMenuSource::SnippetsOnly) - ); + if let Some(excerpt_range) = cursor_excerpt { + let all_edits_within_excerpt = buffer.read_with(cx, |buffer, _| { + let excerpt_range = excerpt_range.context.to_offset(buffer); + buffer + .edited_ranges_for_transaction::(transaction) + .all(|range| { + excerpt_range.start <= range.start && excerpt_range.end >= range.end + }) + }); - if let Some(CodeContextMenu::Completions(menu)) = self.context_menu.borrow_mut().as_mut() { - if filter_completions { - menu.filter( - query.clone().unwrap_or_default(), - buffer_position, - &buffer, - provider.clone(), - window, - cx, - ); - } - // When `is_incomplete` is false, no need to re-query completions when the current query - // is a suffix of the initial query. - let was_complete = !menu.is_incomplete; - if was_complete && !was_snippets_only { - // If the new query is a suffix of the old query (typing more characters) and - // the previous result was complete, the existing completions can be filtered. - // - // Note that snippet completions are always complete. - let query_matches = match (&menu.initial_query, &query) { - (Some(initial_query), Some(query)) => query.starts_with(initial_query.as_ref()), - (None, _) => true, - _ => false, - }; - if query_matches { - let position_matches = if menu.initial_position == position { - true - } else { - let snapshot = self.buffer.read(cx).read(cx); - menu.initial_position.to_offset(&snapshot) == position.to_offset(&snapshot) - }; - if position_matches { - return; - } + if all_edits_within_excerpt { + return Ok(()); } } - }; - - let (word_replace_range, word_to_exclude) = if let (word_range, Some(CharKind::Word)) = - buffer_snapshot.surrounding_word(buffer_position, None) - { - let word_to_exclude = buffer_snapshot - .text_for_range(word_range.clone()) - .collect::(); - ( - buffer_snapshot.anchor_before(word_range.start) - ..buffer_snapshot.anchor_after(buffer_position), - Some(word_to_exclude), - ) - } else { - (buffer_position..buffer_position, None) - }; - - let show_completion_documentation = buffer_snapshot - .settings_at(buffer_position, cx) - .show_completion_documentation; - - // The document can be large, so stay in reasonable bounds when searching for words, - // otherwise completion pop-up might be slow to appear. - const WORD_LOOKUP_ROWS: u32 = 5_000; - let buffer_row = text::ToPoint::to_point(&buffer_position, &buffer_snapshot).row; - let min_word_search = buffer_snapshot.clip_point( - Point::new(buffer_row.saturating_sub(WORD_LOOKUP_ROWS), 0), - Bias::Left, - ); - let max_word_search = buffer_snapshot.clip_point( - Point::new(buffer_row + WORD_LOOKUP_ROWS, 0).min(buffer_snapshot.max_point()), - Bias::Right, - ); - let word_search_range = buffer_snapshot.point_to_offset(min_word_search) - ..buffer_snapshot.point_to_offset(max_word_search); - - let skip_digits = query - .as_ref() - .is_none_or(|query| !query.chars().any(|c| c.is_digit(10))); - - let load_provider_completions = provider.as_ref().is_some_and(|provider| { - trigger.as_ref().is_none_or(|trigger| { - provider.is_completion_trigger( - &buffer, - buffer_position, - trigger, - trigger_in_words, - cx, - ) - }) - }); - - let provider_responses = if let Some(provider) = &provider - && load_provider_completions - { - let trigger_character = trigger - .as_ref() - .filter(|trigger| { - buffer - .read(cx) - .completion_triggers() - .contains(trigger.as_str()) - }) - .cloned(); - let completion_context = CompletionContext { - trigger_kind: match &trigger_character { - Some(_) => CompletionTriggerKind::TRIGGER_CHARACTER, - None => CompletionTriggerKind::INVOKED, - }, - trigger_character, - }; - - provider.completions(&buffer, buffer_position, completion_context, window, cx) - } else { - Task::ready(Ok(Vec::new())) - }; - - let load_word_completions = if !self.word_completions_enabled { - false - } else if requested_source - == Some(CompletionsMenuSource::Words { - ignore_threshold: true, - }) - { - true - } else { - load_provider_completions - && completion_settings.words != WordsCompletionMode::Disabled - && (ignore_word_threshold || { - let words_min_length = completion_settings.words_min_length; - // check whether word has at least `words_min_length` characters - let query_chars = query.iter().flat_map(|q| q.chars()); - query_chars.take(words_min_length).count() == words_min_length - }) - }; - - let mut words = if load_word_completions { - cx.background_spawn({ - let buffer_snapshot = buffer_snapshot.clone(); - async move { - buffer_snapshot.words_in_range(WordsQuery { - fuzzy_contents: None, - range: word_search_range, - skip_digits, - }) - } - }) - } else { - Task::ready(BTreeMap::default()) - }; - - let snippet_char_classifier = buffer_snapshot - .char_classifier_at(buffer_position) - .scope_context(Some(CharScopeContext::Completion)); - - let snippets = if let Some(provider) = &provider - && provider.show_snippets() - && let Some(project) = self.project() - { - let word_trigger = trigger.as_ref().is_some_and(|trigger| { - !trigger.is_empty() - && trigger - .chars() - .all(|character| snippet_char_classifier.is_word(character)) - }); - let requires_strong_snippet_match = !menu_is_open && !trigger_in_words && word_trigger; - let load_snippet_completions = !requires_strong_snippet_match - || query.as_ref().is_some_and(|query| { - let project = project.read(cx); - has_strong_snippet_prefix_match( - &project, - &buffer, - buffer_position, - &snippet_char_classifier, - query, - cx, - ) - }); - - if load_snippet_completions { - project.update(cx, |project, cx| { - snippet_completions( - project, - &buffer, - buffer_position, - snippet_char_classifier, - cx, - ) - }) - } else { - Task::ready(Ok(CompletionResponse { - completions: Vec::new(), - display_options: Default::default(), - is_incomplete: false, - })) - } - } else { - Task::ready(Ok(CompletionResponse { - completions: Vec::new(), - display_options: Default::default(), - is_incomplete: false, - })) - }; - - let snippet_sort_order = EditorSettings::get_global(cx).snippet_sort_order; - - let id = post_inc(&mut self.next_completion_id); - let task = cx.spawn_in(window, async move |editor, cx| { - let Ok(()) = editor.update(cx, |this, _| { - this.completion_tasks.retain(|(task_id, _)| *task_id >= id); - }) else { - return; - }; - - // TODO: Ideally completions from different sources would be selectively re-queried, so - // that having one source with `is_incomplete: true` doesn't cause all to be re-queried. - let mut completions = Vec::new(); - let mut is_incomplete = false; - let mut display_options: Option = None; - if let Some(provider_responses) = provider_responses.await.log_err() - && !provider_responses.is_empty() - { - for response in provider_responses { - completions.extend(response.completions); - is_incomplete = is_incomplete || response.is_incomplete; - match display_options.as_mut() { - None => { - display_options = Some(response.display_options); - } - Some(options) => options.merge(&response.display_options), - } - } - if completion_settings.words == WordsCompletionMode::Fallback { - words = Task::ready(BTreeMap::default()); - } - } - let display_options = display_options.unwrap_or_default(); - - let mut words = words.await; - if let Some(word_to_exclude) = &word_to_exclude { - words.remove(word_to_exclude); - } - for lsp_completion in &completions { - words.remove(&lsp_completion.new_text); - } - completions.extend(words.into_iter().map(|(word, word_range)| Completion { - replace_range: word_replace_range.clone(), - new_text: word.clone(), - label: CodeLabel::plain(word, None), - match_start: None, - snippet_deduplication_key: None, - icon_path: None, - documentation: None, - source: CompletionSource::BufferWord { - word_range, - resolved: false, - }, - insert_text_mode: Some(InsertTextMode::AS_IS), - confirm: None, - })); - - completions.extend( - snippets - .await - .into_iter() - .flat_map(|response| response.completions), - ); - - let menu = if completions.is_empty() { - None - } else { - let Ok((mut menu, matches_task)) = editor.update(cx, |editor, cx| { - let languages = editor - .workspace - .as_ref() - .and_then(|(workspace, _)| workspace.upgrade()) - .map(|workspace| workspace.read(cx).app_state().languages.clone()); - let menu = CompletionsMenu::new( - id, - requested_source.unwrap_or(if load_provider_completions { - CompletionsMenuSource::Normal - } else { - CompletionsMenuSource::SnippetsOnly - }), - sort_completions, - show_completion_documentation, - position, - query.clone(), - is_incomplete, - buffer.clone(), - completions.into(), - editor - .context_menu() - .borrow_mut() - .as_ref() - .map(|menu| menu.primary_scroll_handle()), - display_options, - snippet_sort_order, - languages, - language, - cx, - ); - - let query = if filter_completions { query } else { None }; - let matches_task = menu.do_async_filtering( - query.unwrap_or_default(), - buffer_position, - &buffer, - cx, - ); - (menu, matches_task) - }) else { - return; - }; - - let matches = matches_task.await; - - let Ok(()) = editor.update_in(cx, |editor, window, cx| { - // Newer menu already set, so exit. - if let Some(CodeContextMenu::Completions(prev_menu)) = - editor.context_menu.borrow().as_ref() - && prev_menu.id > id - { - return; - }; - - // Only valid to take prev_menu because either the new menu is immediately set - // below, or the menu is hidden. - if let Some(CodeContextMenu::Completions(prev_menu)) = - editor.context_menu.borrow_mut().take() - { - let position_matches = - if prev_menu.initial_position == menu.initial_position { - true - } else { - let snapshot = editor.buffer.read(cx).read(cx); - prev_menu.initial_position.to_offset(&snapshot) - == menu.initial_position.to_offset(&snapshot) - }; - if position_matches { - // Preserve markdown cache before `set_filter_results` because it will - // try to populate the documentation cache. - menu.preserve_markdown_cache(prev_menu); - } - }; - - menu.set_filter_results(matches, provider, window, cx); - }) else { - return; - }; - - menu.visible().then_some(menu) - }; - - editor - .update_in(cx, |editor, window, cx| { - if editor.focus_handle.is_focused(window) - && let Some(menu) = menu - { - *editor.context_menu.borrow_mut() = - Some(CodeContextMenu::Completions(menu)); - - crate::hover_popover::hide_hover(editor, cx); - if editor.show_edit_predictions_in_menu() { - editor.update_visible_edit_prediction(window, cx); - } else { - editor - .discard_edit_prediction(EditPredictionDiscardReason::Ignored, cx); - } - - cx.notify(); - return; - } - - if editor.completion_tasks.len() <= 1 { - // If there are no more completion tasks and the last menu was empty, we should hide it. - let was_hidden = editor.hide_context_menu(window, cx).is_none(); - // If it was already hidden and we don't show edit predictions in the menu, - // we should also show the edit prediction when available. - if was_hidden && editor.show_edit_predictions_in_menu() { - editor.update_visible_edit_prediction(window, cx); - } - } - }) - .ok(); - }); - - self.completion_tasks.push((id, task)); - } - - #[cfg(any(test, feature = "test-support"))] - pub fn current_completions(&self) -> Option> { - let menu = self.context_menu.borrow(); - if let CodeContextMenu::Completions(menu) = menu.as_ref()? { - let completions = menu.completions.borrow(); - Some(completions.to_vec()) - } else { - None - } - } - - pub fn with_completions_menu_matching_id( - &self, - id: CompletionId, - f: impl FnOnce(Option<&mut CompletionsMenu>) -> R, - ) -> R { - let mut context_menu = self.context_menu.borrow_mut(); - let Some(CodeContextMenu::Completions(completions_menu)) = &mut *context_menu else { - return f(None); - }; - if completions_menu.id != id { - return f(None); - } - f(Some(completions_menu)) - } - - pub fn confirm_completion( - &mut self, - action: &ConfirmCompletion, - window: &mut Window, - cx: &mut Context, - ) -> Option>> { - if self.read_only(cx) { - return None; - } - self.do_completion(action.item_ix, CompletionIntent::Complete, window, cx) - } - - pub fn confirm_completion_insert( - &mut self, - _: &ConfirmCompletionInsert, - window: &mut Window, - cx: &mut Context, - ) -> Option>> { - if self.read_only(cx) { - return None; - } - self.do_completion(None, CompletionIntent::CompleteWithInsert, window, cx) - } - - pub fn confirm_completion_replace( - &mut self, - _: &ConfirmCompletionReplace, - window: &mut Window, - cx: &mut Context, - ) -> Option>> { - if self.read_only(cx) { - return None; - } - self.do_completion(None, CompletionIntent::CompleteWithReplace, window, cx) - } - - pub fn compose_completion( - &mut self, - action: &ComposeCompletion, - window: &mut Window, - cx: &mut Context, - ) -> Option>> { - self.do_completion(action.item_ix, CompletionIntent::Compose, window, cx) - } - - fn do_completion( - &mut self, - item_ix: Option, - intent: CompletionIntent, - window: &mut Window, - cx: &mut Context, - ) -> Option>> { - use language::ToOffset as _; - - let CodeContextMenu::Completions(completions_menu) = self.hide_context_menu(window, cx)? - else { - return None; - }; - - let candidate_id = { - let entries = completions_menu.entries.borrow(); - let mat = entries.get(item_ix.unwrap_or(completions_menu.selected_item))?; - if self.show_edit_predictions_in_menu() { - self.discard_edit_prediction(EditPredictionDiscardReason::Rejected, cx); - } - mat.candidate_id - }; - - let completion = completions_menu - .completions - .borrow() - .get(candidate_id)? - .clone(); - cx.stop_propagation(); - - let buffer_handle = completions_menu.buffer.clone(); - let multibuffer_snapshot = self.buffer.read(cx).snapshot(cx); - let (initial_position, _) = - multibuffer_snapshot.anchor_to_buffer_anchor(completions_menu.initial_position)?; - - let CompletionEdit { - new_text, - snippet, - replace_range, - } = process_completion_for_edit(&completion, intent, &buffer_handle, &initial_position, cx); - - let buffer = buffer_handle.read(cx).snapshot(); - let newest_selection = self.selections.newest_anchor(); - - let Some(replace_range_multibuffer) = - multibuffer_snapshot.buffer_anchor_range_to_anchor_range(replace_range.clone()) - else { - return None; - }; - - let Some((buffer_snapshot, newest_range_buffer)) = - multibuffer_snapshot.anchor_range_to_buffer_anchor_range(newest_selection.range()) - else { - return None; - }; - - let old_text = buffer - .text_for_range(replace_range.clone()) - .collect::(); - let lookbehind = newest_range_buffer - .start - .to_offset(buffer_snapshot) - .saturating_sub(replace_range.start.to_offset(&buffer_snapshot)); - let lookahead = replace_range - .end - .to_offset(&buffer_snapshot) - .saturating_sub(newest_range_buffer.end.to_offset(&buffer)); - let prefix = &old_text[..old_text.len().saturating_sub(lookahead)]; - let suffix = &old_text[lookbehind.min(old_text.len())..]; - - let selections = self - .selections - .all::(&self.display_snapshot(cx)); - let mut ranges = Vec::new(); - let mut all_commit_ranges = Vec::new(); - let mut linked_edits = LinkedEdits::new(); - - let text: Arc = new_text.clone().into(); - for selection in &selections { - let range = if selection.id == newest_selection.id { - replace_range_multibuffer.clone() - } else { - let mut range = selection.range(); - - // if prefix is present, don't duplicate it - if multibuffer_snapshot - .contains_str_at(range.start.saturating_sub_usize(lookbehind), prefix) - { - range.start = range.start.saturating_sub_usize(lookbehind); - - // if suffix is also present, mimic the newest cursor and replace it - if selection.id != newest_selection.id - && multibuffer_snapshot.contains_str_at(range.end, suffix) - { - range.end += lookahead; - } - } - range.to_anchors(&multibuffer_snapshot) - }; - - ranges.push(range.clone()); - - let start_anchor = multibuffer_snapshot.anchor_before(range.start); - let end_anchor = multibuffer_snapshot.anchor_after(range.end); - - if let Some((buffer_snapshot_2, anchor_range)) = - multibuffer_snapshot.anchor_range_to_buffer_anchor_range(start_anchor..end_anchor) - && buffer_snapshot_2.remote_id() == buffer_snapshot.remote_id() - { - all_commit_ranges.push(anchor_range.clone()); - if !self.linked_edit_ranges.is_empty() { - linked_edits.push(&self, anchor_range, text.clone(), cx); - } - } - } - - let common_prefix_len = old_text - .chars() - .zip(new_text.chars()) - .take_while(|(a, b)| a == b) - .map(|(a, _)| a.len_utf8()) - .sum::(); - - cx.emit(EditorEvent::InputHandled { - utf16_range_to_replace: None, - text: new_text[common_prefix_len..].into(), - }); - - let tx_id = self.transact(window, cx, |editor, window, cx| { - if let Some(mut snippet) = snippet { - snippet.text = new_text.to_string(); - let offset_ranges = ranges - .iter() - .map(|range| range.to_offset(&multibuffer_snapshot)) - .collect::>(); - editor - .insert_snippet(&offset_ranges, snippet, window, cx) - .log_err(); - } else { - editor.buffer.update(cx, |multi_buffer, cx| { - let auto_indent = match completion.insert_text_mode { - Some(InsertTextMode::AS_IS) => None, - _ => editor.autoindent_mode.clone(), - }; - let edits = ranges.into_iter().map(|range| (range, new_text.as_str())); - multi_buffer.edit(edits, auto_indent, cx); - }); - } - linked_edits.apply(cx); - editor.refresh_edit_prediction(true, false, window, cx); - }); - self.invalidate_autoclose_regions( - &self.selections.disjoint_anchors_arc(), - &multibuffer_snapshot, - ); - - let show_new_completions_on_confirm = completion - .confirm - .as_ref() - .is_some_and(|confirm| confirm(intent, window, cx)); - if show_new_completions_on_confirm { - self.open_or_update_completions_menu(None, None, false, window, cx); - } - - let provider = self.completion_provider.as_ref()?; - - let lsp_store = self.project().map(|project| project.read(cx).lsp_store()); - let command = lsp_store.as_ref().and_then(|lsp_store| { - let CompletionSource::Lsp { - lsp_completion, - server_id, - .. - } = &completion.source - else { - return None; - }; - let lsp_command = lsp_completion.command.as_ref()?; - let available_commands = lsp_store - .read(cx) - .lsp_server_capabilities - .get(server_id) - .and_then(|server_capabilities| { - server_capabilities - .execute_command_provider - .as_ref() - .map(|options| options.commands.as_slice()) - })?; - if available_commands.contains(&lsp_command.command) { - Some(CodeAction { - server_id: *server_id, - range: language::Anchor::min_min_range_for_buffer(buffer.remote_id()), - lsp_action: LspAction::Command(lsp_command.clone()), - resolved: false, - }) - } else { - None - } - }); - - drop(completion); - let apply_edits = provider.apply_additional_edits_for_completion( - buffer_handle.clone(), - completions_menu.completions.clone(), - candidate_id, - true, - all_commit_ranges, - cx, - ); - - let editor_settings = EditorSettings::get_global(cx); - if editor_settings.show_signature_help_after_edits || editor_settings.auto_signature_help { - // After the code completion is finished, users often want to know what signatures are needed. - // so we should automatically call signature_help - self.show_signature_help(&ShowSignatureHelp, window, cx); - } - - Some(cx.spawn_in(window, async move |editor, cx| { - let additional_edits_tx = apply_edits.await?; - - if let Some((lsp_store, command)) = lsp_store.zip(command) { - let title = command.lsp_action.title().to_owned(); - let project_transaction = lsp_store - .update(cx, |lsp_store, cx| { - lsp_store.apply_code_action(buffer_handle, command, false, cx) - }) - .await - .context("applying post-completion command")?; - if let Some(workspace) = editor.read_with(cx, |editor, _| editor.workspace())? { - Self::open_project_transaction( - &editor, - workspace.downgrade(), - project_transaction, - title, - cx, - ) - .await?; - } - } - - if let Some(tx_id) = tx_id - && let Some(additional_edits_tx) = additional_edits_tx - { - editor - .update(cx, |editor, cx| { - editor.buffer.update(cx, |buffer, cx| { - buffer.merge_transactions(additional_edits_tx.id, tx_id, cx) - }); - }) - .context("merge transactions")?; - } - - Ok(()) - })) - } - - /// Toggles an action selection menu for the latest selection. - /// May show LSP code actions, code lens' command, runnables and potentially more entities applicable as actions. - /// Previous menu toggled with this method will be closed. - pub fn toggle_code_actions( - &mut self, - action: &ToggleCodeActions, - window: &mut Window, - cx: &mut Context, - ) { - let quick_launch = action.quick_launch; - let mut context_menu = self.context_menu.borrow_mut(); - if let Some(CodeContextMenu::CodeActions(code_actions)) = context_menu.as_ref() { - if code_actions.deployed_from == action.deployed_from { - // Toggle if we're selecting the same one - *context_menu = None; - cx.notify(); - return; - } else { - // Otherwise, clear it and start a new one - *context_menu = None; - cx.notify(); - } - } - drop(context_menu); - let snapshot = self.snapshot(window, cx); - let deployed_from = action.deployed_from.clone(); - let action = action.clone(); - self.completion_tasks.clear(); - self.discard_edit_prediction(EditPredictionDiscardReason::Ignored, cx); - - let multibuffer_point = match &action.deployed_from { - Some(CodeActionSource::Indicator(row)) | Some(CodeActionSource::RunMenu(row)) => { - DisplayPoint::new(*row, 0).to_point(&snapshot) - } - _ => self - .selections - .newest::(&snapshot.display_snapshot) - .head(), - }; - let Some((buffer, buffer_row)) = snapshot - .buffer_snapshot() - .buffer_line_for_row(MultiBufferRow(multibuffer_point.row)) - .and_then(|(buffer_snapshot, range)| { - self.buffer() - .read(cx) - .buffer(buffer_snapshot.remote_id()) - .map(|buffer| (buffer, range.start.row)) - }) - else { - return; - }; - let buffer_id = buffer.read(cx).remote_id(); - let tasks = self - .runnables - .runnables((buffer_id, buffer_row)) - .map(|t| Arc::new(t.to_owned())); - - let project = self.project.clone(); - let runnable_task = match deployed_from { - Some(CodeActionSource::Indicator(_)) => Task::ready(Ok(Default::default())), - _ => { - let mut task_context_task = Task::ready(Ok(None)); - let workspace = self.workspace().map(|w| w.downgrade()); - if let Some(tasks) = &tasks - && let Some(project) = project - { - task_context_task = - Self::build_tasks_context(&project, &buffer, buffer_row, tasks, cx); - } - - cx.spawn_in(window, { - let buffer = buffer.clone(); - async move |editor, cx| { - let task_context = match workspace { - Some(ws) => task_context_task - .await - .notify_workspace_async_err(ws, cx) - .flatten(), - None => task_context_task.await.ok().flatten(), - }; - - let resolved_tasks = - tasks - .zip(task_context.clone()) - .map(|(tasks, task_context)| ResolvedTasks { - templates: tasks.resolve(&task_context).collect(), - position: snapshot.buffer_snapshot().anchor_before(Point::new( - multibuffer_point.row, - tasks.column, - )), - }); - let debug_scenarios = editor - .update(cx, |editor, cx| { - editor.debug_scenarios(&resolved_tasks, &buffer, cx) - })? - .await; - anyhow::Ok((resolved_tasks, debug_scenarios, task_context)) - } - }) - } - }; - - let toggle_task = cx.spawn_in(window, async move |editor, cx| { - let (resolved_tasks, debug_scenarios, task_context) = runnable_task.await?; - - let code_actions = if let Some(CodeActionSource::RunMenu(_)) = &deployed_from { - None - } else { - editor.update(cx, |editor, _cx| match &editor.code_actions_for_selection { - CodeActionsForSelection::None => None, - CodeActionsForSelection::Fetching(task) => Some(task.clone()), - CodeActionsForSelection::Ready(action_fetch_ready) => { - Some(Task::ready(Some(action_fetch_ready.clone())).shared()) - } - })? - }; - let code_actions = match code_actions { - Some(code_actions) => code_actions - .await - .filter(|ActionFetchReady { location, .. }| { - let snapshot = location.buffer.read_with(cx, |buffer, _| buffer.snapshot()); - let point_range = location.range.to_point(&snapshot); - (point_range.start.row..=point_range.end.row).contains(&buffer_row) - }) - .map(|ActionFetchReady { actions, .. }| actions), - None => None, - }; - - editor.update_in(cx, |editor, window, cx| { - let spawn_straight_away = quick_launch - && resolved_tasks - .as_ref() - .is_some_and(|tasks| tasks.templates.len() == 1) - && code_actions - .as_ref() - .is_none_or(|actions| actions.is_empty()) - && debug_scenarios.is_empty(); - - crate::hover_popover::hide_hover(editor, cx); - let actions = CodeActionContents::new( - resolved_tasks, - code_actions, - debug_scenarios, - task_context.unwrap_or_default(), - ); - - // Don't show the menu if there are no actions available - if actions.is_empty() { - cx.notify(); - return Task::ready(Ok(())); - } - - *editor.context_menu.borrow_mut() = - Some(CodeContextMenu::CodeActions(CodeActionsMenu { - buffer, - actions, - selected_item: Default::default(), - scroll_handle: UniformListScrollHandle::default(), - deployed_from, - })); - cx.notify(); - if spawn_straight_away - && let Some(task) = editor.confirm_code_action( - &ConfirmCodeAction { item_ix: Some(0) }, - window, - cx, - ) - { - return task; - } - - Task::ready(Ok(())) - }) - }); - self.runnables_for_selection_toggle = cx.background_spawn(async move { - match toggle_task.await { - Ok(code_action_spawn) => match code_action_spawn.await { - Ok(()) => {} - Err(e) => log::error!("failed to spawn a toggled code action: {e:#}"), - }, - Err(e) => log::error!("failed to toggle code actions: {e:#}"), - } - }) - } - - fn debug_scenarios( - &mut self, - resolved_tasks: &Option, - buffer: &Entity, - cx: &mut App, - ) -> Task> { - maybe!({ - let project = self.project()?; - let dap_store = project.read(cx).dap_store(); - let mut scenarios = vec![]; - let resolved_tasks = resolved_tasks.as_ref()?; - let buffer = buffer.read(cx); - let language = buffer.language()?; - let debug_adapter = LanguageSettings::for_buffer(&buffer, cx) - .debuggers - .first() - .map(SharedString::from) - .or_else(|| language.config().debuggers.first().map(SharedString::from))?; - - dap_store.update(cx, |dap_store, cx| { - for (_, task) in &resolved_tasks.templates { - let maybe_scenario = dap_store.debug_scenario_for_build_task( - task.original_task().clone(), - debug_adapter.clone().into(), - task.display_label().to_owned().into(), - cx, - ); - scenarios.push(maybe_scenario); - } - }); - Some(cx.background_spawn(async move { - futures::future::join_all(scenarios) - .await - .into_iter() - .flatten() - .collect::>() - })) - }) - .unwrap_or_else(|| Task::ready(vec![])) - } - - pub fn confirm_code_action( - &mut self, - action: &ConfirmCodeAction, - window: &mut Window, - cx: &mut Context, - ) -> Option>> { - if self.read_only(cx) { - return None; - } - - let actions_menu = - if let CodeContextMenu::CodeActions(menu) = self.hide_context_menu(window, cx)? { - menu - } else { - return None; - }; - - let action_ix = action.item_ix.unwrap_or(actions_menu.selected_item); - let action = actions_menu.actions.get(action_ix)?; - let title = action.label(); - let buffer = actions_menu.buffer; - let workspace = self.workspace()?; - - match action { - CodeActionsItem::Task(task_source_kind, resolved_task) => { - workspace.update(cx, |workspace, cx| { - workspace.schedule_resolved_task( - task_source_kind, - resolved_task, - false, - window, - cx, - ); - - Some(Task::ready(Ok(()))) - }) - } - CodeActionsItem::CodeAction { action, provider } => { - if code_lens::try_handle_client_command(&action, self, &workspace, window, cx) { - return Some(Task::ready(Ok(()))); - } - - let apply_code_action = - provider.apply_code_action(buffer, action, true, window, cx); - let workspace = workspace.downgrade(); - Some(cx.spawn_in(window, async move |editor, cx| { - let project_transaction = apply_code_action.await?; - Self::open_project_transaction( - &editor, - workspace, - project_transaction, - title, - cx, - ) - .await - })) - } - CodeActionsItem::DebugScenario(scenario) => { - let context = actions_menu.actions.context.into(); - - workspace.update(cx, |workspace, cx| { - dap::send_telemetry(&scenario, TelemetrySpawnLocation::Gutter, cx); - workspace.start_debug_session( - scenario, - context, - Some(buffer), - None, - window, - cx, - ); - }); - Some(Task::ready(Ok(()))) - } - } - } - - fn open_transaction_for_hidden_buffers( - workspace: Entity, - transaction: ProjectTransaction, - title: String, - window: &mut Window, - cx: &mut Context, - ) { - if transaction.0.is_empty() { - return; - } - - let edited_buffers_already_open = { - let other_editors: Vec> = workspace - .read(cx) - .panes() - .iter() - .flat_map(|pane| pane.read(cx).items_of_type::()) - .filter(|editor| editor.entity_id() != cx.entity_id()) - .collect(); - - transaction.0.keys().all(|buffer| { - other_editors.iter().any(|editor| { - let multi_buffer = editor.read(cx).buffer(); - multi_buffer.read(cx).is_singleton() - && multi_buffer - .read(cx) - .as_singleton() - .map_or(false, |singleton| { - singleton.entity_id() == buffer.entity_id() - }) - }) - }) - }; - if !edited_buffers_already_open { - let workspace = workspace.downgrade(); - cx.defer_in(window, move |_, window, cx| { - cx.spawn_in(window, async move |editor, cx| { - Self::open_project_transaction(&editor, workspace, transaction, title, cx) - .await - .ok() - }) - .detach(); - }); - } - } - - pub async fn open_project_transaction( - editor: &WeakEntity, - workspace: WeakEntity, - transaction: ProjectTransaction, - title: String, - cx: &mut AsyncWindowContext, - ) -> Result<()> { - let mut entries = transaction.0.into_iter().collect::>(); - cx.update(|_, cx| { - entries.sort_unstable_by_key(|(buffer, _)| { - buffer.read(cx).file().map(|f| f.path().clone()) - }); - })?; - if entries.is_empty() { - return Ok(()); - } - - // If the project transaction's edits are all contained within this editor, then - // avoid opening a new editor to display them. - - if let [(buffer, transaction)] = &*entries { - let cursor_excerpt = editor.update(cx, |editor, cx| { - let snapshot = editor.buffer().read(cx).snapshot(cx); - let head = editor.selections.newest_anchor().head(); - let (buffer_snapshot, excerpt_range) = snapshot.excerpt_containing(head..head)?; - if buffer_snapshot.remote_id() != buffer.read(cx).remote_id() { - return None; - } - Some(excerpt_range) - })?; - - if let Some(excerpt_range) = cursor_excerpt { - let all_edits_within_excerpt = buffer.read_with(cx, |buffer, _| { - let excerpt_range = excerpt_range.context.to_offset(buffer); - buffer - .edited_ranges_for_transaction::(transaction) - .all(|range| { - excerpt_range.start <= range.start && excerpt_range.end >= range.end - }) - }); - - if all_edits_within_excerpt { - return Ok(()); - } - } - } + } let mut ranges_to_highlight = Vec::new(); let excerpt_buffer = cx.new(|cx| { @@ -7304,204 +5987,29 @@ impl Editor { let text_range = buffer_snapshot.anchor_range_inside(range); let start = snapshot.anchor_in_buffer(text_range.start)?; let end = snapshot.anchor_in_buffer(text_range.end)?; - Some(start..end) - })); - } - multibuffer.push_transaction(entries.iter().map(|(b, t)| (b, t)), cx); - multibuffer - }); - - workspace.update_in(cx, |workspace, window, cx| { - let project = workspace.project().clone(); - let editor = - cx.new(|cx| Editor::for_multibuffer(excerpt_buffer, Some(project), window, cx)); - workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx); - editor.update(cx, |editor, cx| { - editor.highlight_background( - HighlightKey::Editor, - &ranges_to_highlight, - |_, theme| theme.colors().editor_highlighted_line_background, - cx, - ); - }); - })?; - - Ok(()) - } - - pub fn add_code_action_provider( - &mut self, - provider: Rc, - window: &mut Window, - cx: &mut Context, - ) { - if self - .code_action_providers - .iter() - .any(|existing_provider| existing_provider.id() == provider.id()) - { - return; - } - - self.code_action_providers.push(provider); - self.refresh_code_actions_for_selection(window, cx); - } - - pub fn remove_code_action_provider( - &mut self, - id: Arc, - window: &mut Window, - cx: &mut Context, - ) { - self.code_action_providers - .retain(|provider| provider.id() != id); - self.refresh_code_actions_for_selection(window, cx); - } - - pub fn code_actions_enabled_for_toolbar(&self, cx: &App) -> bool { - !self.code_action_providers.is_empty() - && EditorSettings::get_global(cx).toolbar.code_actions - } - - pub fn has_available_code_actions_for_selection(&self) -> bool { - if let CodeActionsForSelection::Ready(ready) = &self.code_actions_for_selection { - !ready.actions.is_empty() - } else { - false - } - } - - fn render_inline_code_actions( - &self, - icon_size: ui::IconSize, - display_row: DisplayRow, - is_active: bool, - cx: &mut Context, - ) -> AnyElement { - let show_tooltip = !self.context_menu_visible(); - IconButton::new("inline_code_actions", ui::IconName::BoltFilled) - .icon_size(icon_size) - .shape(ui::IconButtonShape::Square) - .icon_color(ui::Color::Hidden) - .toggle_state(is_active) - .when(show_tooltip, |this| { - this.tooltip({ - let focus_handle = self.focus_handle.clone(); - move |_window, cx| { - Tooltip::for_action_in( - "Toggle Code Actions", - &ToggleCodeActions { - deployed_from: None, - quick_launch: false, - }, - &focus_handle, - cx, - ) - } - }) - }) - .on_click(cx.listener(move |editor, _: &ClickEvent, window, cx| { - window.focus(&editor.focus_handle(cx), cx); - editor.toggle_code_actions( - &crate::actions::ToggleCodeActions { - deployed_from: Some(crate::actions::CodeActionSource::Indicator( - display_row, - )), - quick_launch: false, - }, - window, - cx, - ); - })) - .into_any_element() - } - - pub fn context_menu(&self) -> &RefCell> { - &self.context_menu - } - - fn refresh_code_actions_for_selection(&mut self, window: &mut Window, cx: &mut Context) { - self.code_actions_for_selection = CodeActionsForSelection::Fetching( - cx.spawn_in(window, async move |editor, cx| { - cx.background_executor() - .timer(CODE_ACTIONS_DEBOUNCE_TIMEOUT) - .await; - - let (start_buffer, start, _, end, _newest_selection) = editor - .update(cx, |editor, cx| { - let newest_selection = editor.selections.newest_anchor().clone(); - if newest_selection.head().diff_base_anchor().is_some() { - return None; - } - let display_snapshot = editor.display_snapshot(cx); - let newest_selection_adjusted = - editor.selections.newest_adjusted(&display_snapshot); - let buffer = editor.buffer.read(cx); - - let (start_buffer, start) = - buffer.text_anchor_for_position(newest_selection_adjusted.start, cx)?; - let (end_buffer, end) = - buffer.text_anchor_for_position(newest_selection_adjusted.end, cx)?; - - Some((start_buffer, start, end_buffer, end, newest_selection)) - }) - .ok() - .flatten() - .filter(|(start_buffer, _, end_buffer, _, _)| start_buffer == end_buffer)?; - - let (providers, tasks) = editor - .update_in(cx, |editor, window, cx| { - let providers = editor.code_action_providers.clone(); - let tasks = editor - .code_action_providers - .iter() - .map(|provider| { - provider.code_actions(&start_buffer, start..end, window, cx) - }) - .collect::>(); - (providers, tasks) - }) - .ok()?; + Some(start..end) + })); + } + multibuffer.push_transaction(entries.iter().map(|(b, t)| (b, t)), cx); + multibuffer + }); - let mut actions = Vec::new(); - for (provider, provider_actions) in - providers.into_iter().zip(future::join_all(tasks).await) - { - if let Some(provider_actions) = provider_actions.log_err() { - actions.extend(provider_actions.into_iter().map(|action| { - AvailableCodeAction { - action, - provider: provider.clone(), - } - })); - } - } + workspace.update_in(cx, |workspace, window, cx| { + let project = workspace.project().clone(); + let editor = + cx.new(|cx| Editor::for_multibuffer(excerpt_buffer, Some(project), window, cx)); + workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx); + editor.update(cx, |editor, cx| { + editor.highlight_background( + HighlightKey::Editor, + &ranges_to_highlight, + |_, theme| theme.colors().editor_highlighted_line_background, + cx, + ); + }); + })?; - editor - .update(cx, |editor, cx| { - let new_actions = if actions.is_empty() { - editor.code_actions_for_selection = CodeActionsForSelection::None; - None - } else { - let new_actions = ActionFetchReady { - location: Location { - buffer: start_buffer, - range: start..end, - }, - actions: Rc::from(actions), - }; - editor.code_actions_for_selection = - CodeActionsForSelection::Ready(new_actions.clone()); - Some(new_actions) - }; - cx.notify(); - new_actions - }) - .ok() - .flatten() - }) - .shared(), - ); + Ok(()) } fn start_inline_blame_timer(&mut self, window: &mut Window, cx: &mut Context) { @@ -19752,10 +18260,6 @@ impl Editor { window.show_character_palette(); } - pub fn disable_word_completions(&mut self) { - self.word_completions_enabled = false; - } - pub fn toggle_minimap( &mut self, _: &ToggleMinimap, @@ -24932,13 +23436,6 @@ impl Editor { Some(gpui::Point::new(source_x, source_y)) } - pub fn has_visible_completions_menu(&self) -> bool { - !self.edit_prediction_preview_is_active() - && self.context_menu.borrow().as_ref().is_some_and(|menu| { - menu.visible() && matches!(menu, CodeContextMenu::Completions(_)) - }) - } - pub fn register_addon(&mut self, instance: T) { if self.mode.is_minimap() { return; @@ -26242,478 +24739,6 @@ pub trait SemanticsProvider { ) -> Option>>; } -pub trait CompletionProvider { - fn completions( - &self, - buffer: &Entity, - buffer_position: text::Anchor, - trigger: CompletionContext, - window: &mut Window, - cx: &mut Context, - ) -> Task>>; - - fn resolve_completions( - &self, - _buffer: Entity, - _completion_indices: Vec, - _completions: Rc>>, - _cx: &mut Context, - ) -> Task> { - Task::ready(Ok(false)) - } - - fn apply_additional_edits_for_completion( - &self, - _buffer: Entity, - _completions: Rc>>, - _completion_index: usize, - _push_to_history: bool, - _all_commit_ranges: Vec>, - _cx: &mut Context, - ) -> Task>> { - Task::ready(Ok(None)) - } - - fn is_completion_trigger( - &self, - buffer: &Entity, - position: language::Anchor, - text: &str, - trigger_in_words: bool, - cx: &mut Context, - ) -> bool; - - fn selection_changed(&self, _mat: Option<&StringMatch>, _window: &mut Window, _cx: &mut App) {} - - fn sort_completions(&self) -> bool { - true - } - - fn filter_completions(&self) -> bool { - true - } - - fn show_snippets(&self) -> bool { - false - } -} - -pub trait CodeActionProvider { - fn id(&self) -> Arc; - - fn code_actions( - &self, - buffer: &Entity, - range: Range, - window: &mut Window, - cx: &mut App, - ) -> Task>>; - - fn apply_code_action( - &self, - buffer_handle: Entity, - action: CodeAction, - push_to_history: bool, - window: &mut Window, - cx: &mut App, - ) -> Task>; -} - -impl CodeActionProvider for Entity { - fn id(&self) -> Arc { - "project".into() - } - - fn code_actions( - &self, - buffer: &Entity, - range: Range, - _window: &mut Window, - cx: &mut App, - ) -> Task>> { - self.update(cx, |project, cx| { - let code_lens_actions = if EditorSettings::get_global(cx).code_lens.show_in_menu() { - Some(project.code_lens_actions(buffer, range.clone(), cx)) - } else { - None - }; - let code_actions = project.code_actions(buffer, range, None, cx); - cx.background_spawn(async move { - let code_lens_actions = match code_lens_actions { - Some(task) => task.await.context("code lens fetch")?.unwrap_or_default(), - None => Vec::new(), - }; - let code_actions = code_actions - .await - .context("code action fetch")? - .unwrap_or_default(); - Ok(code_lens_actions.into_iter().chain(code_actions).collect()) - }) - }) - } - - fn apply_code_action( - &self, - buffer_handle: Entity, - action: CodeAction, - push_to_history: bool, - _window: &mut Window, - cx: &mut App, - ) -> Task> { - self.update(cx, |project, cx| { - project.apply_code_action(buffer_handle, action, push_to_history, cx) - }) - } -} - -fn has_strong_snippet_prefix_match( - project: &Project, - buffer: &Entity, - buffer_anchor: text::Anchor, - classifier: &CharClassifier, - query: &str, - cx: &App, -) -> bool { - if query.chars().take(2).count() < 2 { - return false; - } - - let query = query.to_lowercase(); - let is_word_char = |character| classifier.is_word(character); - let languages = buffer.read(cx).languages_at(buffer_anchor); - let snippet_store = project.snippets().read(cx); - - languages.iter().any(|language| { - snippet_store - .snippets_for(Some(language.lsp_id()), cx) - .iter() - .flat_map(|snippet| snippet.prefix.iter()) - .flat_map(|prefix| snippet_candidate_suffixes(prefix, &is_word_char)) - .any(|candidate| candidate.to_lowercase().starts_with(&query)) - }) -} - -fn snippet_completions( - project: &Project, - buffer: &Entity, - buffer_anchor: text::Anchor, - classifier: CharClassifier, - cx: &mut App, -) -> Task> { - let languages = buffer.read(cx).languages_at(buffer_anchor); - let snippet_store = project.snippets().read(cx); - - let scopes: Vec<_> = languages - .iter() - .filter_map(|language| { - let language_name = language.lsp_id(); - let snippets = snippet_store.snippets_for(Some(language_name), cx); - - if snippets.is_empty() { - None - } else { - Some((language.default_scope(), snippets)) - } - }) - .collect(); - - if scopes.is_empty() { - return Task::ready(Ok(CompletionResponse { - completions: vec![], - display_options: CompletionDisplayOptions::default(), - is_incomplete: false, - })); - } - - let snapshot = buffer.read(cx).text_snapshot(); - let executor = cx.background_executor().clone(); - - cx.background_spawn(async move { - let is_word_char = |c| classifier.is_word(c); - - let mut is_incomplete = false; - let mut completions: Vec = Vec::new(); - - const MAX_PREFIX_LEN: usize = 128; - let buffer_offset = text::ToOffset::to_offset(&buffer_anchor, &snapshot); - let window_start = buffer_offset.saturating_sub(MAX_PREFIX_LEN); - let window_start = snapshot.clip_offset(window_start, Bias::Left); - - let max_buffer_window: String = snapshot - .text_for_range(window_start..buffer_offset) - .collect(); - - if max_buffer_window.is_empty() { - return Ok(CompletionResponse { - completions: vec![], - display_options: CompletionDisplayOptions::default(), - is_incomplete: true, - }); - } - - for (_scope, snippets) in scopes.into_iter() { - // Sort snippets by word count to match longer snippet prefixes first. - let mut sorted_snippet_candidates = snippets - .iter() - .enumerate() - .flat_map(|(snippet_ix, snippet)| { - snippet - .prefix - .iter() - .enumerate() - .map(move |(prefix_ix, prefix)| { - let word_count = - snippet_candidate_suffixes(prefix, &is_word_char).count(); - ((snippet_ix, prefix_ix), prefix, word_count) - }) - }) - .collect_vec(); - sorted_snippet_candidates - .sort_unstable_by_key(|(_, _, word_count)| Reverse(*word_count)); - - // Each prefix may be matched multiple times; the completion menu must filter out duplicates. - - let buffer_windows = snippet_candidate_suffixes(&max_buffer_window, &is_word_char) - .take( - sorted_snippet_candidates - .first() - .map(|(_, _, word_count)| *word_count) - .unwrap_or_default(), - ) - .collect_vec(); - - const MAX_RESULTS: usize = 100; - // Each match also remembers how many characters from the buffer it consumed - let mut matches: Vec<(StringMatch, usize)> = vec![]; - - let mut snippet_list_cutoff_index = 0; - for (buffer_index, buffer_window) in buffer_windows.iter().enumerate().rev() { - let word_count = buffer_index + 1; - // Increase `snippet_list_cutoff_index` until we have all of the - // snippets with sufficiently many words. - while sorted_snippet_candidates - .get(snippet_list_cutoff_index) - .is_some_and(|(_ix, _prefix, snippet_word_count)| { - *snippet_word_count >= word_count - }) - { - snippet_list_cutoff_index += 1; - } - - // Take only the candidates with at least `word_count` many words - let snippet_candidates_at_word_len = - &sorted_snippet_candidates[..snippet_list_cutoff_index]; - - let candidates = snippet_candidates_at_word_len - .iter() - .map(|(_snippet_ix, prefix, _snippet_word_count)| prefix) - .enumerate() // index in `sorted_snippet_candidates` - // First char must match - .filter(|(_ix, prefix)| { - itertools::equal( - prefix - .chars() - .next() - .into_iter() - .flat_map(|c| c.to_lowercase()), - buffer_window - .chars() - .next() - .into_iter() - .flat_map(|c| c.to_lowercase()), - ) - }) - .map(|(ix, prefix)| StringMatchCandidate::new(ix, prefix)) - .collect::>(); - - matches.extend( - fuzzy::match_strings( - &candidates, - &buffer_window, - buffer_window.chars().any(|c| c.is_uppercase()), - true, - MAX_RESULTS - matches.len(), // always prioritize longer snippets - &Default::default(), - executor.clone(), - ) - .await - .into_iter() - .map(|string_match| (string_match, buffer_window.len())), - ); - - if matches.len() >= MAX_RESULTS { - break; - } - } - - let to_lsp = |point: &text::Anchor| { - let end = text::ToPointUtf16::to_point_utf16(point, &snapshot); - point_to_lsp(end) - }; - let lsp_end = to_lsp(&buffer_anchor); - - if matches.len() >= MAX_RESULTS { - is_incomplete = true; - } - - completions.extend(matches.iter().map(|(string_match, buffer_window_len)| { - let ((snippet_index, prefix_index), matching_prefix, _snippet_word_count) = - sorted_snippet_candidates[string_match.candidate_id]; - let snippet = &snippets[snippet_index]; - let start = buffer_offset - buffer_window_len; - let start = snapshot.anchor_before(start); - let range = start..buffer_anchor; - let lsp_start = to_lsp(&start); - let lsp_range = lsp::Range { - start: lsp_start, - end: lsp_end, - }; - Completion { - replace_range: range, - new_text: snippet.body.clone(), - source: CompletionSource::Lsp { - insert_range: None, - server_id: LanguageServerId(usize::MAX), - resolved: true, - lsp_completion: Box::new(lsp::CompletionItem { - label: snippet.prefix.first().unwrap().clone(), - kind: Some(CompletionItemKind::SNIPPET), - label_details: snippet.description.as_ref().map(|description| { - lsp::CompletionItemLabelDetails { - detail: Some(description.clone()), - description: None, - } - }), - insert_text_format: Some(InsertTextFormat::SNIPPET), - text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace( - lsp::InsertReplaceEdit { - new_text: snippet.body.clone(), - insert: lsp_range, - replace: lsp_range, - }, - )), - filter_text: Some(snippet.body.clone()), - sort_text: Some(char::MAX.to_string()), - ..lsp::CompletionItem::default() - }), - lsp_defaults: None, - }, - label: CodeLabel { - text: matching_prefix.clone(), - runs: Vec::new(), - filter_range: 0..matching_prefix.len(), - }, - icon_path: None, - documentation: Some(CompletionDocumentation::SingleLineAndMultiLinePlainText { - single_line: snippet.name.clone().into(), - plain_text: snippet - .description - .clone() - .map(|description| description.into()), - }), - insert_text_mode: None, - confirm: None, - match_start: Some(start), - snippet_deduplication_key: Some((snippet_index, prefix_index)), - } - })); - } - - Ok(CompletionResponse { - completions, - display_options: CompletionDisplayOptions::default(), - is_incomplete, - }) - }) -} - -impl CompletionProvider for Entity { - fn completions( - &self, - buffer: &Entity, - buffer_position: text::Anchor, - options: CompletionContext, - _window: &mut Window, - cx: &mut Context, - ) -> Task>> { - self.update(cx, |project, cx| { - let task = project.completions(buffer, buffer_position, options, cx); - cx.background_spawn(task) - }) - } - - fn resolve_completions( - &self, - buffer: Entity, - completion_indices: Vec, - completions: Rc>>, - cx: &mut Context, - ) -> Task> { - self.update(cx, |project, cx| { - project.lsp_store().update(cx, |lsp_store, cx| { - lsp_store.resolve_completions(buffer, completion_indices, completions, cx) - }) - }) - } - - fn apply_additional_edits_for_completion( - &self, - buffer: Entity, - completions: Rc>>, - completion_index: usize, - push_to_history: bool, - all_commit_ranges: Vec>, - cx: &mut Context, - ) -> Task>> { - self.update(cx, |project, cx| { - project.lsp_store().update(cx, |lsp_store, cx| { - lsp_store.apply_additional_edits_for_completion( - buffer, - completions, - completion_index, - push_to_history, - all_commit_ranges, - cx, - ) - }) - }) - } - - fn is_completion_trigger( - &self, - buffer: &Entity, - position: language::Anchor, - text: &str, - trigger_in_words: bool, - cx: &mut Context, - ) -> bool { - let mut chars = text.chars(); - let char = if let Some(char) = chars.next() { - char - } else { - return false; - }; - if chars.next().is_some() { - return false; - } - - let buffer = buffer.read(cx); - let snapshot = buffer.snapshot(); - let classifier = snapshot - .char_classifier_at(position) - .scope_context(Some(CharScopeContext::Completion)); - if trigger_in_words && classifier.is_word(char) { - return true; - } - - buffer.completion_triggers().contains(text) - } - - fn show_snippets(&self) -> bool { - true - } -} - impl SemanticsProvider for WeakEntity { fn hover( &self, @@ -28115,53 +26140,6 @@ pub fn styled_runs_for_code_label<'a>( ) } -pub(crate) fn split_words(text: &str) -> impl std::iter::Iterator + '_ { - let mut prev_index = 0; - let mut prev_codepoint: Option = None; - text.char_indices() - .chain([(text.len(), '\0')]) - .filter_map(move |(index, codepoint)| { - let prev_codepoint = prev_codepoint.replace(codepoint)?; - let is_boundary = index == text.len() - || !prev_codepoint.is_uppercase() && codepoint.is_uppercase() - || !prev_codepoint.is_alphanumeric() && codepoint.is_alphanumeric(); - if is_boundary { - let chunk = &text[prev_index..index]; - prev_index = index; - Some(chunk) - } else { - None - } - }) -} - -/// Given a string of text immediately before the cursor, iterates over possible -/// strings a snippet could match to. More precisely: returns an iterator over -/// suffixes of `text` created by splitting at word boundaries (before & after -/// every non-word character). -/// -/// Shorter suffixes are returned first. -pub(crate) fn snippet_candidate_suffixes<'a>( - text: &'a str, - is_word_char: &'a dyn Fn(char) -> bool, -) -> impl std::iter::Iterator + 'a { - let mut prev_index = text.len(); - let mut prev_codepoint = None; - text.char_indices() - .rev() - .chain([(0, '\0')]) - .filter_map(move |(index, codepoint)| { - let prev_index = std::mem::replace(&mut prev_index, index); - let prev_codepoint = prev_codepoint.replace(codepoint)?; - if is_word_char(prev_codepoint) && is_word_char(codepoint) { - None - } else { - let chunk = &text[prev_index..]; // go to end of string - Some(chunk) - } - }) -} - pub trait RangeToAnchorExt: Sized { fn to_anchors(self, snapshot: &MultiBufferSnapshot) -> Range;