Stop eagerly clearing available code actions on row change (#53571)

This commit is contained in:
João Soares authored and GitHub committed 2026-04-22 20:03:14 +03:00
1 parent 76883bb983
commit 828a64c3da
3 files changed
+165 -154

No files matched your search

+150 -142
View File
@@ -1127,6 +1127,18 @@ pub(crate) struct DiffReviewOverlay {
_subscription: Subscription,
}
enum CodeActionsForSelection {
None,
Fetching(Shared<Task<Option<ActionFetchReady>>>),
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<bool>,
find_all_references_task_sources: Vec<Anchor>,
next_completion_id: CompletionId,
available_code_actions: Option<(Location, Rc<[AvailableCodeAction]>)>,
code_actions_task: Option<Task<Result<()>>>,
code_actions_for_selection: CodeActionsForSelection,
runnables_for_selection_toggle: Task<()>,
quick_selection_highlight_task: Option<(Range<Anchor>, Task<()>)>,
debounced_selection_highlight_task: Option<(Range<Anchor>, 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<Self>,
) -> Task<Option<Rc<[AvailableCodeAction]>>> {
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<dyn CodeActionProvider>,
@@ -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>) {
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>) {
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) =
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::<Vec<_>>();
(providers, tasks)
})?;
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 (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);
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();
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 start_inline_blame_timer(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@@ -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);
+3 -1
View File
@@ -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()
+12 -11
View File
@@ -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,
);
})
}
}),
)