From 828a64c3da6919b5d0ba67195e9e2d7e6fb2eeb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Soares?= <37777652+Dnreikronos@users.noreply.github.com> Date: Wed, 22 Apr 2026 14:03:14 -0300 Subject: [PATCH] Stop eagerly clearing available code actions on row change (#53571) --- crates/editor/src/editor.rs | 286 +++++++++++++------------ crates/editor/src/element.rs | 4 +- crates/zed/src/zed/quick_action_bar.rs | 23 +- 3 files changed, 162 insertions(+), 151 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 405f1f5752cd799e433efea87600a3072600e18c..57115bb12636efced0cee356ff440c1017a429bb 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1127,6 +1127,18 @@ pub(crate) struct DiffReviewOverlay { _subscription: Subscription, } +enum CodeActionsForSelection { + None, + Fetching(Shared>>), + Ready(ActionFetchReady), +} + +#[derive(Clone)] +struct ActionFetchReady { + location: Location, + actions: Rc<[AvailableCodeAction]>, +} + /// Zed's primary implementation of text input, allowing users to edit a [`MultiBuffer`]. /// /// See the [module level documentation](self) for more information. @@ -1215,8 +1227,8 @@ pub struct Editor { auto_signature_help: Option, find_all_references_task_sources: Vec, next_completion_id: CompletionId, - available_code_actions: Option<(Location, Rc<[AvailableCodeAction]>)>, - code_actions_task: Option>>, + code_actions_for_selection: CodeActionsForSelection, + runnables_for_selection_toggle: Task<()>, quick_selection_highlight_task: Option<(Range, Task<()>)>, debounced_selection_highlight_task: Option<(Range, Task<()>)>, debounced_selection_highlight_complete: bool, @@ -2231,7 +2243,7 @@ impl Editor { editor.update_lsp_data(Some(buffer_id), window, cx); editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); refresh_linked_ranges(editor, window, cx); - editor.refresh_code_actions(window, cx); + editor.refresh_code_actions_for_selection(window, cx); editor.refresh_document_highlights(cx); } } @@ -2479,8 +2491,8 @@ impl Editor { next_completion_id: 0, next_inlay_id: 0, code_action_providers, - available_code_actions: None, - code_actions_task: None, + code_actions_for_selection: CodeActionsForSelection::None, + runnables_for_selection_toggle: Task::ready(()), quick_selection_highlight_task: None, debounced_selection_highlight_task: None, debounced_selection_highlight_complete: false, @@ -3805,12 +3817,7 @@ impl Editor { hide_hover(self, cx); - if old_cursor_position.to_display_point(&display_map).row() - != new_cursor_position.to_display_point(&display_map).row() - { - self.available_code_actions.take(); - } - self.refresh_code_actions(window, cx); + self.refresh_code_actions_for_selection(window, cx); self.refresh_document_highlights(cx); refresh_linked_ranges(self, window, cx); @@ -6950,6 +6957,9 @@ 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, @@ -7004,16 +7014,7 @@ impl Editor { .runnables((buffer_id, buffer_row)) .map(|t| Arc::new(t.to_owned())); - if !self.focus_handle.is_focused(window) { - return; - } let project = self.project.clone(); - - let code_actions_task = match deployed_from { - Some(CodeActionSource::RunMenu(_)) => Task::ready(None), - _ => self.code_actions(buffer_row, window, cx), - }; - let runnable_task = match deployed_from { Some(CodeActionSource::Indicator(_)) => Task::ready(Ok(Default::default())), _ => { @@ -7051,19 +7052,42 @@ impl Editor { } }; - cx.spawn_in(window, async move |editor, cx| { + let toggle_task = cx.spawn_in(window, async move |editor, cx| { let (resolved_tasks, debug_scenarios, task_context) = runnable_task.await?; - let code_actions = code_actions_task.await; - 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(); + + 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, @@ -7099,8 +7123,16 @@ impl Editor { 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:#}"), + } }) - .detach_and_log_err(cx); } fn debug_scenarios( @@ -7144,42 +7176,6 @@ impl Editor { .unwrap_or_else(|| Task::ready(vec![])) } - fn code_actions( - &mut self, - buffer_row: u32, - window: &mut Window, - cx: &mut Context, - ) -> Task>> { - let mut task = self.code_actions_task.take(); - cx.spawn_in(window, async move |editor, cx| { - while let Some(prev_task) = task { - prev_task.await.log_err(); - task = editor - .update(cx, |this, _| this.code_actions_task.take()) - .ok()?; - } - - editor - .update(cx, |editor, cx| { - editor - .available_code_actions - .clone() - .and_then(|(location, code_actions)| { - let snapshot = location.buffer.read(cx).snapshot(); - let point_range = location.range.to_point(&snapshot); - let point_range = point_range.start.row..=point_range.end.row; - if point_range.contains(&buffer_row) { - Some(code_actions) - } else { - None - } - }) - }) - .ok() - .flatten() - }) - } - pub fn confirm_code_action( &mut self, action: &ConfirmCodeAction, @@ -7393,11 +7389,6 @@ impl Editor { Ok(()) } - pub fn clear_code_action_providers(&mut self) { - self.code_action_providers.clear(); - self.available_code_actions.take(); - } - pub fn add_code_action_provider( &mut self, provider: Rc, @@ -7413,7 +7404,7 @@ impl Editor { } self.code_action_providers.push(provider); - self.refresh_code_actions(window, cx); + self.refresh_code_actions_for_selection(window, cx); } pub fn remove_code_action_provider( @@ -7424,7 +7415,7 @@ impl Editor { ) { self.code_action_providers .retain(|provider| provider.id() != id); - self.refresh_code_actions(window, cx); + self.refresh_code_actions_for_selection(window, cx); } pub fn code_actions_enabled_for_toolbar(&self, cx: &App) -> bool { @@ -7432,10 +7423,12 @@ impl Editor { && EditorSettings::get_global(cx).toolbar.code_actions } - pub fn has_available_code_actions(&self) -> bool { - self.available_code_actions - .as_ref() - .is_some_and(|(_, actions)| !actions.is_empty()) + pub fn has_available_code_actions_for_selection(&self) -> bool { + if let CodeActionsForSelection::Ready(ready) = &self.code_actions_for_selection { + !ready.actions.is_empty() + } else { + false + } } fn render_inline_code_actions( @@ -7487,73 +7480,88 @@ impl Editor { &self.context_menu } - fn refresh_code_actions(&mut self, window: &mut Window, cx: &mut Context) { - self.code_actions_task = Some(cx.spawn_in(window, async move |this, cx| { - cx.background_executor() - .timer(CODE_ACTIONS_DEBOUNCE_TIMEOUT) - .await; + fn refresh_code_actions_for_selection(&mut self, window: &mut Window, cx: &mut Context) { + self.code_actions_for_selection = CodeActionsForSelection::Fetching( + cx.spawn_in(window, async move |editor, cx| { + cx.background_executor() + .timer(CODE_ACTIONS_DEBOUNCE_TIMEOUT) + .await; - let (start_buffer, start, _, end, _newest_selection) = this - .update(cx, |this, cx| { - let newest_selection = this.selections.newest_anchor().clone(); - if newest_selection.head().diff_base_anchor().is_some() { - return None; - } - let display_snapshot = this.display_snapshot(cx); - let newest_selection_adjusted = - this.selections.newest_adjusted(&display_snapshot); - let buffer = this.buffer.read(cx); + 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)?; + 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)) - })? - .filter(|(start_buffer, _, end_buffer, _, _)| start_buffer == end_buffer) - .context( - "Expected selection to lie in a single buffer when refreshing code actions", - )?; - let (providers, tasks) = this.update_in(cx, |this, window, cx| { - let providers = this.code_action_providers.clone(); - let tasks = this - .code_action_providers - .iter() - .map(|provider| provider.code_actions(&start_buffer, start..end, window, cx)) - .collect::>(); - (providers, tasks) - })?; + Some((start_buffer, start, end_buffer, end, newest_selection)) + }) + .ok() + .flatten() + .filter(|(start_buffer, _, end_buffer, _, _)| start_buffer == end_buffer)?; - 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(), - } - })); + let (providers, tasks) = editor + .update_in(cx, |editor, window, cx| { + let providers = editor.code_action_providers.clone(); + let tasks = editor + .code_action_providers + .iter() + .map(|provider| { + provider.code_actions(&start_buffer, start..end, window, cx) + }) + .collect::>(); + (providers, tasks) + }) + .ok()?; + + let mut actions = Vec::new(); + for (provider, provider_actions) in + providers.into_iter().zip(future::join_all(tasks).await) + { + if let Some(provider_actions) = provider_actions.log_err() { + actions.extend(provider_actions.into_iter().map(|action| { + AvailableCodeAction { + action, + provider: provider.clone(), + } + })); + } } - } - this.update(cx, |this, cx| { - this.available_code_actions = if actions.is_empty() { - None - } else { - Some(( - Location { - buffer: start_buffer, - range: start..end, - }, - actions.into(), - )) - }; - cx.notify(); + 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 start_inline_blame_timer(&mut self, window: &mut Window, cx: &mut Context) { @@ -24835,7 +24843,7 @@ impl Editor { self.scrollbar_marker_state.dirty = true; self.active_indent_guides_state.dirty = true; self.refresh_active_diagnostics(cx); - self.refresh_code_actions(window, cx); + self.refresh_code_actions_for_selection(window, cx); self.refresh_single_line_folds(window, cx); let snapshot = self.snapshot(window, cx); self.refresh_matching_bracket_highlights(&snapshot, cx); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index daaad997c761c8082eb3fa1b40495d706ea9fdca..0b1fe2841795412f9bceb5b6717f9ead094b5d6a 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2538,7 +2538,9 @@ impl EditorElement { let icon_size = ui::IconSize::XSmall; let mut button = self.editor.update(cx, |editor, cx| { - editor.available_code_actions.as_ref()?; + if !editor.has_available_code_actions_for_selection() { + return None; + } let active = editor .context_menu .borrow() diff --git a/crates/zed/src/zed/quick_action_bar.rs b/crates/zed/src/zed/quick_action_bar.rs index 83fd77ccc484c69dea5632c1b6492af2aaf802b5..0f6864e6fa33af8283355638b8d3098dc5370719 100644 --- a/crates/zed/src/zed/quick_action_bar.rs +++ b/crates/zed/src/zed/quick_action_bar.rs @@ -133,7 +133,7 @@ impl Render for QuickActionBar { editor_value.edit_predictions_enabled_at_cursor(cx); let supports_minimap = editor_value.supports_minimap(cx); let minimap_enabled = supports_minimap && editor_value.minimap().is_some(); - let has_available_code_actions = editor_value.has_available_code_actions(); + let has_available_code_actions = editor_value.has_available_code_actions_for_selection(); let code_action_enabled = editor_value.code_actions_enabled_for_toolbar(cx); let focus_handle = editor_value.focus_handle(cx); @@ -169,7 +169,6 @@ impl Render for QuickActionBar { ); let code_actions_dropdown = code_action_enabled.then(|| { - let focus = editor.focus_handle(cx); let is_deployed = { let menu_ref = editor.read(cx).context_menu().borrow(); let code_action_menu = menu_ref @@ -211,16 +210,18 @@ impl Render for QuickActionBar { ) }) .on_click({ - let focus = focus; + let editor = editor.clone(); move |_, window, cx| { - focus.dispatch_action( - &ToggleCodeActions { - deployed_from: Some(CodeActionSource::QuickActionBar), - quick_launch: false, - }, - window, - cx, - ); + editor.update(cx, |editor, cx| { + editor.toggle_code_actions( + &ToggleCodeActions { + deployed_from: Some(CodeActionSource::QuickActionBar), + quick_launch: false, + }, + window, + cx, + ); + }) } }), )