editor: Extract `completions` and `code_actions` out of `editor.rs` (#56030)

Mikhail Pertsev created

cc @SomeoneToIgnore

## Summary

Follow-up to https://github.com/zed-industries/zed/discussions/55352,
where the conclusion was to split `editor.rs` incrementally by topic
instead of all at once.

This mechanically extracts two editor topics into focused sibling
modules:

- `crates/editor/src/code_actions.rs`
- `crates/editor/src/completions.rs`

One odd boundary remains: `Editor::context_menu()` is still a general
context-menu accessor, but it now lives in `code_actions.rs` because it
was part of the moved code actions block and is also used by
completions, Vim tests, agent UI, and the quick action bar. Would you
prefer that generic context-menu accessor stay in `editor.rs` for now
until context-menu code gets its own extraction?

## Testing

- `cargo check -p editor --lib`
- `cargo check -p editor --tests`
- `cargo check -p editor --lib --features test-support`

Self-Review Checklist:

- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- N/A

Change summary

crates/editor/src/code_actions.rs |  523 +++++++++++
crates/editor/src/completions.rs  | 1489 +++++++++++++++++++++++++++++++++
crates/editor/src/editor.rs       | 1481 +-------------------------------
3 files changed, 2,094 insertions(+), 1,399 deletions(-)

Detailed changes

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<Self>,
+    ) {
+        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::<Point>(&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<Self>,
+    ) -> Option<Task<Result<()>>> {
+        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<Option<CodeContextMenu>> {
+        &self.context_menu
+    }
+
+    pub(super) fn render_inline_code_actions(
+        &self,
+        icon_size: ui::IconSize,
+        display_row: DisplayRow,
+        is_active: bool,
+        cx: &mut Context<Self>,
+    ) -> 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>,
+    ) {
+        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::<Vec<_>>();
+                        (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<ResolvedTasks>,
+        buffer: &Entity<Buffer>,
+        cx: &mut App,
+    ) -> Task<Vec<task::DebugScenario>> {
+        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::<Vec<_>>()
+            }))
+        })
+        .unwrap_or_else(|| Task::ready(vec![]))
+    }
+}
+
+pub trait CodeActionProvider {
+    fn id(&self) -> Arc<str>;
+
+    fn code_actions(
+        &self,
+        buffer: &Entity<Buffer>,
+        range: Range<text::Anchor>,
+        window: &mut Window,
+        cx: &mut App,
+    ) -> Task<Result<Vec<CodeAction>>>;
+
+    fn apply_code_action(
+        &self,
+        buffer_handle: Entity<Buffer>,
+        action: CodeAction,
+        push_to_history: bool,
+        window: &mut Window,
+        cx: &mut App,
+    ) -> Task<Result<ProjectTransaction>>;
+}
+
+impl CodeActionProvider for Entity<Project> {
+    fn id(&self) -> Arc<str> {
+        "project".into()
+    }
+
+    fn code_actions(
+        &self,
+        buffer: &Entity<Buffer>,
+        range: Range<text::Anchor>,
+        _window: &mut Window,
+        cx: &mut App,
+    ) -> Task<Result<Vec<CodeAction>>> {
+        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<Buffer>,
+        action: CodeAction,
+        push_to_history: bool,
+        _window: &mut Window,
+        cx: &mut App,
+    ) -> Task<Result<ProjectTransaction>> {
+        self.update(cx, |project, cx| {
+            project.apply_code_action(buffer_handle, action, push_to_history, cx)
+        })
+    }
+}

crates/editor/src/completions.rs 🔗

@@ -0,0 +1,1489 @@
+use super::*;
+
+impl Editor {
+    pub fn set_completion_provider(&mut self, provider: Option<Rc<dyn CompletionProvider>>) {
+        self.completion_provider = provider;
+    }
+
+    pub fn set_show_completions_on_input(&mut self, show_completions_on_input: Option<bool>) {
+        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>,
+    ) {
+        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>,
+    ) {
+        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<Self>,
+    ) -> Option<Task<Result<()>>> {
+        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<Self>,
+    ) -> Option<Task<Result<()>>> {
+        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<Self>,
+    ) -> Option<Task<Result<()>>> {
+        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<Self>,
+    ) -> Option<Task<Result<()>>> {
+        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<Self>,
+    ) {
+        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<dyn language::File>>, 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<Editor>) -> Vec<Entity<Buffer>> {
+        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<Editor>,
+    ) -> Vec<(
+        BufferSnapshot,
+        Range<BufferOffset>,
+        ExcerptRange<text::Anchor>,
+    )> {
+        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<Self>,
+    ) -> Option<Task<Result<()>>> {
+        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<CompletionsMenuSource>,
+        trigger: Option<String>,
+        trigger_in_words: bool,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        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<Arc<String>> =
+            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::<String>();
+            (
+                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<CompletionDisplayOptions> = 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<R>(
+        &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<String> {
+        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::<String>(),
+            )
+        } else {
+            None
+        }
+    }
+
+    fn do_completion(
+        &mut self,
+        item_ix: Option<usize>,
+        intent: CompletionIntent,
+        window: &mut Window,
+        cx: &mut Context<Editor>,
+    ) -> Option<Task<Result<()>>> {
+        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::<String>();
+        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::<MultiBufferOffset>(&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<str> = 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::<usize>();
+
+        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::<Vec<_>>();
+                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<Rc<dyn CompletionProvider>> {
+        self.completion_provider.clone()
+    }
+
+    pub fn current_completions(&self) -> Option<Vec<project::Completion>> {
+        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>,
+        buffer_position: text::Anchor,
+        trigger: CompletionContext,
+        window: &mut Window,
+        cx: &mut Context<Editor>,
+    ) -> Task<Result<Vec<CompletionResponse>>>;
+
+    fn resolve_completions(
+        &self,
+        _buffer: Entity<Buffer>,
+        _completion_indices: Vec<usize>,
+        _completions: Rc<RefCell<Box<[Completion]>>>,
+        _cx: &mut Context<Editor>,
+    ) -> Task<Result<bool>> {
+        Task::ready(Ok(false))
+    }
+
+    fn apply_additional_edits_for_completion(
+        &self,
+        _buffer: Entity<Buffer>,
+        _completions: Rc<RefCell<Box<[Completion]>>>,
+        _completion_index: usize,
+        _push_to_history: bool,
+        _all_commit_ranges: Vec<Range<language::Anchor>>,
+        _cx: &mut Context<Editor>,
+    ) -> Task<Result<Option<language::Transaction>>> {
+        Task::ready(Ok(None))
+    }
+
+    fn is_completion_trigger(
+        &self,
+        buffer: &Entity<Buffer>,
+        position: language::Anchor,
+        text: &str,
+        trigger_in_words: bool,
+        cx: &mut Context<Editor>,
+    ) -> 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>,
+    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>,
+    buffer_anchor: text::Anchor,
+    classifier: CharClassifier,
+    cx: &mut App,
+) -> Task<Result<CompletionResponse>> {
+    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<Completion> = 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::<Vec<StringMatchCandidate>>();
+
+                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<Project> {
+    fn completions(
+        &self,
+        buffer: &Entity<Buffer>,
+        buffer_position: text::Anchor,
+        options: CompletionContext,
+        _window: &mut Window,
+        cx: &mut Context<Editor>,
+    ) -> Task<Result<Vec<CompletionResponse>>> {
+        self.update(cx, |project, cx| {
+            let task = project.completions(buffer, buffer_position, options, cx);
+            cx.background_spawn(task)
+        })
+    }
+
+    fn resolve_completions(
+        &self,
+        buffer: Entity<Buffer>,
+        completion_indices: Vec<usize>,
+        completions: Rc<RefCell<Box<[Completion]>>>,
+        cx: &mut Context<Editor>,
+    ) -> Task<Result<bool>> {
+        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<Buffer>,
+        completions: Rc<RefCell<Box<[Completion]>>>,
+        completion_index: usize,
+        push_to_history: bool,
+        all_commit_ranges: Vec<Range<language::Anchor>>,
+        cx: &mut Context<Editor>,
+    ) -> Task<Result<Option<language::Transaction>>> {
+        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<Buffer>,
+        position: language::Anchor,
+        text: &str,
+        trigger_in_words: bool,
+        cx: &mut Context<Editor>,
+    ) -> 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<Item = &str> + '_ {
+    let mut prev_index = 0;
+    let mut prev_codepoint: Option<char> = 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<Item = &'a str> + '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)
+            }
+        })
+}

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<Rc<dyn CompletionProvider>>) {
-        self.completion_provider = provider;
-    }
-
-    #[cfg(any(test, feature = "test-support"))]
-    pub fn completion_provider(&self) -> Option<Rc<dyn CompletionProvider>> {
-        self.completion_provider.clone()
-    }
-
     pub fn semantics_provider(&self) -> Option<Rc<dyn SemanticsProvider>> {
         self.semantics_provider.clone()
     }
@@ -3578,10 +3576,6 @@ impl Editor {
         }
     }
 
-    pub fn set_show_completions_on_input(&mut self, show_completions_on_input: Option<bool>) {
-        self.show_completions_on_input_override = show_completions_on_input;
-    }
-
     pub fn set_show_edit_predictions(
         &mut self,
         show_edit_predictions: Option<bool>,
@@ -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<Self>,
-    ) {
-        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<Self>) {
@@ -5917,1371 +5873,98 @@ impl Editor {
         });
     }
 
-    fn completion_query(buffer: &MultiBufferSnapshot, position: impl ToOffset) -> Option<String> {
-        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::<String>(),
-            )
-        } else {
-            None
-        }
-    }
-
-    pub fn is_lsp_relevant(&self, file: Option<&Arc<dyn language::File>>, 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<Editor>) -> Vec<Entity<Buffer>> {
-        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<Editor>,
-    ) -> Vec<(
-        BufferSnapshot,
-        Range<BufferOffset>,
-        ExcerptRange<text::Anchor>,
-    )> {
-        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<Self>,
-    ) -> Option<Task<Result<()>>> {
-        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>,
-    ) {
-        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>,
-    ) {
-        self.open_or_update_completions_menu(None, None, false, window, cx);
-    }
-
-    fn open_or_update_completions_menu(
-        &mut self,
-        requested_source: Option<CompletionsMenuSource>,
-        trigger: Option<String>,
-        trigger_in_words: bool,
+    fn open_transaction_for_hidden_buffers(
+        workspace: Entity<Workspace>,
+        transaction: ProjectTransaction,
+        title: String,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        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<Entity<Editor>> = workspace
+                .read(cx)
+                .panes()
+                .iter()
+                .flat_map(|pane| pane.read(cx).items_of_type::<Editor>())
+                .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<Arc<String>> =
-            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<Editor>,
+        workspace: WeakEntity<Workspace>,
+        transaction: ProjectTransaction,
+        title: String,
+        cx: &mut AsyncWindowContext,
+    ) -> Result<()> {
+        let mut entries = transaction.0.into_iter().collect::<Vec<_>>();
+        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::<usize>(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::<String>();
-            (
-                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<CompletionDisplayOptions> = 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<Vec<project::Completion>> {
-        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<R>(
-        &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<Self>,
-    ) -> Option<Task<Result<()>>> {
-        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<Self>,
-    ) -> Option<Task<Result<()>>> {
-        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<Self>,
-    ) -> Option<Task<Result<()>>> {
-        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<Self>,
-    ) -> Option<Task<Result<()>>> {
-        self.do_completion(action.item_ix, CompletionIntent::Compose, window, cx)
-    }
-
-    fn do_completion(
-        &mut self,
-        item_ix: Option<usize>,
-        intent: CompletionIntent,
-        window: &mut Window,
-        cx: &mut Context<Editor>,
-    ) -> Option<Task<Result<()>>> {
-        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::<String>();
-        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::<MultiBufferOffset>(&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<str> = 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::<usize>();
-
-        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::<Vec<_>>();
-                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<Self>,
-    ) {
-        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::<Point>(&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<ResolvedTasks>,
-        buffer: &Entity<Buffer>,
-        cx: &mut App,
-    ) -> Task<Vec<task::DebugScenario>> {
-        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::<Vec<_>>()
-            }))
-        })
-        .unwrap_or_else(|| Task::ready(vec![]))
-    }
-
-    pub fn confirm_code_action(
-        &mut self,
-        action: &ConfirmCodeAction,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Option<Task<Result<()>>> {
-        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<Workspace>,
-        transaction: ProjectTransaction,
-        title: String,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        if transaction.0.is_empty() {
-            return;
-        }
-
-        let edited_buffers_already_open = {
-            let other_editors: Vec<Entity<Editor>> = workspace
-                .read(cx)
-                .panes()
-                .iter()
-                .flat_map(|pane| pane.read(cx).items_of_type::<Editor>())
-                .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<Editor>,
-        workspace: WeakEntity<Workspace>,
-        transaction: ProjectTransaction,
-        title: String,
-        cx: &mut AsyncWindowContext,
-    ) -> Result<()> {
-        let mut entries = transaction.0.into_iter().collect::<Vec<_>>();
-        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::<usize>(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| {