Fix code actions run confusion (#32579)

Now if you click the triangle you get runnables, if you click the
lightning bolt you get code actions, if you trigger the code actions
menu with the mouse/keyboard you still get both.

Release Notes:

- Fixed the run/code actions menu to not duplicate content when opened
from the respective icons.

---------

Co-authored-by: Anthony Eid <hello@anthonyeid.me>
This commit is contained in:
Conrad Irwin 2025-06-11 16:51:46 -06:00 committed by GitHub
parent 9032ea9849
commit 2a63c5f951
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 175 additions and 150 deletions

View file

@ -87,6 +87,7 @@ pub struct ToggleCodeActions {
#[derive(PartialEq, Clone, Debug)]
pub enum CodeActionSource {
Indicator(DisplayRow),
RunMenu(DisplayRow),
QuickActionBar,
}

View file

@ -1401,7 +1401,9 @@ impl CodeActionsMenu {
fn origin(&self) -> ContextMenuOrigin {
match &self.deployed_from {
Some(CodeActionSource::Indicator(row)) => ContextMenuOrigin::GutterIndicator(*row),
Some(CodeActionSource::Indicator(row)) | Some(CodeActionSource::RunMenu(row)) => {
ContextMenuOrigin::GutterIndicator(*row)
}
Some(CodeActionSource::QuickActionBar) => ContextMenuOrigin::QuickActionBar,
None => ContextMenuOrigin::Cursor,
}

View file

@ -5711,70 +5711,60 @@ impl Editor {
drop(context_menu);
let snapshot = self.snapshot(window, cx);
let deployed_from = action.deployed_from.clone();
let mut task = self.code_actions_task.take();
let action = action.clone();
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())?;
self.completion_tasks.clear();
self.discard_inline_completion(false, cx);
let multibuffer_point = match &action.deployed_from {
Some(CodeActionSource::Indicator(row)) => {
DisplayPoint::new(*row, 0).to_point(&snapshot)
}
_ => self.selections.newest::<Point>(cx).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
.tasks
.get(&(buffer_id, buffer_row))
.map(|t| Arc::new(t.to_owned()));
let spawned_test_task = editor.update_in(cx, |editor, window, cx| {
if editor.focus_handle.is_focused(window) {
let multibuffer_point = match &action.deployed_from {
Some(CodeActionSource::Indicator(row)) => {
DisplayPoint::new(*row, 0).to_point(&snapshot)
}
_ => editor.selections.newest::<Point>(cx).head(),
};
let (buffer, buffer_row) = snapshot
.buffer_snapshot
.buffer_line_for_row(MultiBufferRow(multibuffer_point.row))
.and_then(|(buffer_snapshot, range)| {
editor
.buffer
.read(cx)
.buffer(buffer_snapshot.remote_id())
.map(|buffer| (buffer, range.start.row))
})?;
let (_, code_actions) = 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((location, code_actions))
} else {
None
}
})
.unzip();
let buffer_id = buffer.read(cx).remote_id();
let tasks = editor
.tasks
.get(&(buffer_id, buffer_row))
.map(|t| Arc::new(t.to_owned()));
if tasks.is_none() && code_actions.is_none() {
return None;
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())),
_ => {
let mut task_context_task = Task::ready(None);
if let Some(tasks) = &tasks {
if let Some(project) = project {
task_context_task =
Self::build_tasks_context(&project, &buffer, buffer_row, &tasks, cx);
}
}
editor.completion_tasks.clear();
editor.discard_inline_completion(false, cx);
let task_context =
tasks
.as_ref()
.zip(editor.project.clone())
.map(|(tasks, project)| {
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 = task_context_task.await;
Some(cx.spawn_in(window, async move |editor, cx| {
let task_context = match task_context {
Some(task_context) => task_context.await,
None => None,
};
let resolved_tasks =
tasks
.zip(task_context.clone())
@ -5786,103 +5776,135 @@ impl Editor {
)),
});
let debug_scenarios = editor.update(cx, |editor, cx| {
if cx.has_flag::<DebuggerFeatureFlag>() {
maybe!({
let project = editor.project.as_ref()?;
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 file = buffer.file();
let debug_adapter =
language_settings(language.name().into(), file, 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 {
if let Some(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(scenario);
}
}
});
Some(scenarios)
})
.unwrap_or_default()
} else {
vec![]
}
editor.debug_scenarios(&resolved_tasks, &buffer, cx)
})?;
let spawn_straight_away = quick_launch
&& resolved_tasks
.as_ref()
.map_or(false, |tasks| tasks.templates.len() == 1)
&& code_actions
.as_ref()
.map_or(true, |actions| actions.is_empty())
&& debug_scenarios.is_empty();
if let Ok(task) = editor.update_in(cx, |editor, window, cx| {
crate::hover_popover::hide_hover(editor, cx);
*editor.context_menu.borrow_mut() =
Some(CodeContextMenu::CodeActions(CodeActionsMenu {
buffer,
actions: CodeActionContents::new(
resolved_tasks,
code_actions,
debug_scenarios,
task_context.unwrap_or_default(),
),
selected_item: Default::default(),
scroll_handle: UniformListScrollHandle::default(),
deployed_from,
}));
if spawn_straight_away {
if let Some(task) = editor.confirm_code_action(
&ConfirmCodeAction { item_ix: Some(0) },
window,
cx,
) {
cx.notify();
return task;
}
}
cx.notify();
Task::ready(Ok(()))
}) {
task.await
} else {
Ok(())
}
}))
} else {
Some(Task::ready(Ok(())))
}
})?;
if let Some(task) = spawned_test_task {
task.await?;
anyhow::Ok((resolved_tasks, debug_scenarios, task_context))
}
})
}
};
anyhow::Ok(())
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()
.map_or(false, |tasks| tasks.templates.len() == 1)
&& code_actions
.as_ref()
.map_or(true, |actions| actions.is_empty())
&& debug_scenarios.is_empty();
editor.update_in(cx, |editor, window, cx| {
crate::hover_popover::hide_hover(editor, cx);
*editor.context_menu.borrow_mut() =
Some(CodeContextMenu::CodeActions(CodeActionsMenu {
buffer,
actions: CodeActionContents::new(
resolved_tasks,
code_actions,
debug_scenarios,
task_context.unwrap_or_default(),
),
selected_item: Default::default(),
scroll_handle: UniformListScrollHandle::default(),
deployed_from,
}));
if spawn_straight_away {
if let Some(task) = editor.confirm_code_action(
&ConfirmCodeAction { item_ix: Some(0) },
window,
cx,
) {
cx.notify();
return task;
}
}
Task::ready(Ok(()))
})
})
.detach_and_log_err(cx);
}
fn debug_scenarios(
&mut self,
resolved_tasks: &Option<ResolvedTasks>,
buffer: &Entity<Buffer>,
cx: &mut App,
) -> Vec<task::DebugScenario> {
if cx.has_flag::<DebuggerFeatureFlag>() {
maybe!({
let project = self.project.as_ref()?;
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 file = buffer.file();
let debug_adapter = language_settings(language.name().into(), file, 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 {
if let Some(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(scenario);
}
}
});
Some(scenarios)
})
.unwrap_or_default()
} else {
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,
@ -7920,7 +7942,7 @@ impl Editor {
window.focus(&editor.focus_handle(cx));
editor.toggle_code_actions(
&ToggleCodeActions {
deployed_from: Some(CodeActionSource::Indicator(row)),
deployed_from: Some(CodeActionSource::RunMenu(row)),
quick_launch,
},
window,