Skip to repository content576 lines · 22.8 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T01:31:30.361Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
code_actions.rs
1use super::*;
2
3impl Editor {
4 /// Toggles an action selection menu for the latest selection.
5 /// May show LSP code actions, code lens' command, runnables and potentially more entities applicable as actions.
6 /// Previous menu toggled with this method will be closed.
7 pub fn toggle_code_actions(
8 &mut self,
9 action: &ToggleCodeActions,
10 window: &mut Window,
11 cx: &mut Context<Self>,
12 ) {
13 let quick_launch = action.quick_launch;
14 let mut context_menu = self.context_menu.borrow_mut();
15 if let Some(CodeContextMenu::CodeActions(code_actions)) = context_menu.as_ref() {
16 if code_actions.deployed_from == action.deployed_from {
17 // Toggle if we're selecting the same one
18 *context_menu = None;
19 cx.notify();
20 return;
21 } else {
22 // Otherwise, clear it and start a new one
23 *context_menu = None;
24 cx.notify();
25 }
26 }
27 drop(context_menu);
28 let snapshot = self.snapshot(window, cx);
29 let deployed_from = action.deployed_from.clone();
30 let action = action.clone();
31 self.completion_tasks.clear();
32 self.discard_edit_prediction(EditPredictionDiscardReason::Ignored, cx);
33
34 let multibuffer_point = match &action.deployed_from {
35 Some(CodeActionSource::Indicator(row)) | Some(CodeActionSource::RunMenu(row)) => {
36 DisplayPoint::new(*row, 0).to_point(&snapshot)
37 }
38 _ => self
39 .selections
40 .newest::<Point>(&snapshot.display_snapshot)
41 .head(),
42 };
43 let Some((buffer, buffer_row)) = snapshot
44 .buffer_snapshot()
45 .buffer_line_for_row(MultiBufferRow(multibuffer_point.row))
46 .and_then(|(buffer_snapshot, range)| {
47 self.buffer()
48 .read(cx)
49 .buffer(buffer_snapshot.remote_id())
50 .map(|buffer| (buffer, range.start.row))
51 })
52 else {
53 return;
54 };
55 let buffer_id = buffer.read(cx).remote_id();
56 let tasks = self
57 .runnables
58 .runnables((buffer_id, buffer_row))
59 .map(|t| Arc::new(t.to_owned()));
60
61 let project = self.project.clone();
62 let runnable_task = match deployed_from {
63 Some(CodeActionSource::Indicator(_)) => Task::ready(Ok(Default::default())),
64 _ => {
65 let mut task_context_task = Task::ready(Ok(None));
66 let workspace = self.workspace().map(|w| w.downgrade());
67 if let Some(tasks) = &tasks
68 && let Some(project) = project
69 {
70 task_context_task =
71 Self::build_tasks_context(&project, &buffer, buffer_row, tasks, cx);
72 }
73
74 cx.spawn_in(window, {
75 let buffer = buffer.clone();
76 async move |editor, cx| {
77 let task_context = match workspace {
78 Some(ws) => task_context_task
79 .await
80 .notify_workspace_async_err(ws, cx)
81 .flatten(),
82 None => task_context_task.await.ok().flatten(),
83 };
84
85 let resolved_tasks =
86 tasks
87 .zip(task_context.clone())
88 .map(|(tasks, task_context)| ResolvedTasks {
89 templates: tasks.resolve(&task_context).collect(),
90 position: snapshot.buffer_snapshot().anchor_before(Point::new(
91 multibuffer_point.row,
92 tasks.column,
93 )),
94 });
95 let debug_scenarios = editor
96 .update(cx, |editor, cx| {
97 editor.debug_scenarios(&resolved_tasks, &buffer, cx)
98 })?
99 .await;
100 anyhow::Ok((resolved_tasks, debug_scenarios, task_context))
101 }
102 })
103 }
104 };
105
106 let toggle_task = cx.spawn_in(window, async move |editor, cx| {
107 let (resolved_tasks, debug_scenarios, task_context) = runnable_task.await?;
108
109 let code_actions = if let Some(CodeActionSource::RunMenu(_)) = &deployed_from {
110 None
111 } else {
112 editor.update(cx, |editor, _cx| match &editor.code_actions_for_selection {
113 CodeActionsForSelection::None => None,
114 CodeActionsForSelection::Fetching(task) => Some(task.clone()),
115 CodeActionsForSelection::Ready(action_fetch_ready) => {
116 Some(Task::ready(Some(action_fetch_ready.clone())).shared())
117 }
118 })?
119 };
120 let code_actions = match code_actions {
121 Some(code_actions) => code_actions
122 .await
123 .filter(|ActionFetchReady { location, .. }| {
124 let snapshot = location.buffer.read_with(cx, |buffer, _| buffer.snapshot());
125 let point_range = location.range.to_point(&snapshot);
126 (point_range.start.row..=point_range.end.row).contains(&buffer_row)
127 })
128 .map(|ActionFetchReady { actions, .. }| actions),
129 None => None,
130 };
131
132 editor.update_in(cx, |editor, window, cx| {
133 let spawn_straight_away = quick_launch
134 && resolved_tasks
135 .as_ref()
136 .is_some_and(|tasks| tasks.templates.len() == 1)
137 && code_actions
138 .as_ref()
139 .is_none_or(|actions| actions.is_empty())
140 && debug_scenarios.is_empty();
141
142 crate::hover_popover::hide_hover(editor, cx);
143 let actions = CodeActionContents::new(
144 resolved_tasks,
145 code_actions,
146 debug_scenarios,
147 task_context.unwrap_or_default(),
148 );
149
150 // Don't show the menu if there are no actions available
151 if actions.is_empty() {
152 cx.notify();
153 return Task::ready(Ok(()));
154 }
155
156 *editor.context_menu.borrow_mut() =
157 Some(CodeContextMenu::CodeActions(CodeActionsMenu {
158 buffer,
159 actions,
160 selected_item: Default::default(),
161 scroll_handle: UniformListScrollHandle::default(),
162 deployed_from,
163 }));
164 cx.notify();
165 if spawn_straight_away
166 && let Some(task) = editor.confirm_code_action(
167 &ConfirmCodeAction { item_ix: Some(0) },
168 window,
169 cx,
170 )
171 {
172 return task;
173 }
174
175 Task::ready(Ok(()))
176 })
177 });
178 self.runnables_for_selection_toggle = cx.background_spawn(async move {
179 match toggle_task.await {
180 Ok(code_action_spawn) => match code_action_spawn.await {
181 Ok(()) => {}
182 Err(e) => log::error!("failed to spawn a toggled code action: {e:#}"),
183 },
184 Err(e) => log::error!("failed to toggle code actions: {e:#}"),
185 }
186 })
187 }
188
189 pub fn confirm_code_action(
190 &mut self,
191 action: &ConfirmCodeAction,
192 window: &mut Window,
193 cx: &mut Context<Self>,
194 ) -> Option<Task<Result<()>>> {
195 if self.read_only(cx) {
196 return None;
197 }
198
199 let actions_menu =
200 if let CodeContextMenu::CodeActions(menu) = self.hide_context_menu(window, cx)? {
201 menu
202 } else {
203 return None;
204 };
205
206 let action_ix = action.item_ix.unwrap_or(actions_menu.selected_item);
207 let action = actions_menu.actions.get(action_ix)?;
208 let title = action.label();
209 let runnable_task_key =
210 self.runnable_task_key_for_source(&actions_menu.deployed_from, window, cx);
211 let buffer = actions_menu.buffer;
212 let workspace = self.workspace()?;
213
214 match action {
215 CodeActionsItem::Task(task_source_kind, resolved_task) => {
216 if let Some((buffer_id, buffer_row)) = runnable_task_key {
217 self.set_runnable_task_status(
218 buffer_id,
219 buffer_row,
220 RunnableTaskStatus::Running,
221 cx,
222 );
223 }
224 let editor = cx.weak_entity();
225 workspace.update(cx, |workspace, cx| {
226 if let Some((buffer_id, buffer_row)) = runnable_task_key {
227 workspace.schedule_resolved_task_with_completion(
228 task_source_kind,
229 resolved_task,
230 false,
231 move |result, cx| {
232 editor
233 .update(cx, |editor, cx| {
234 editor.set_runnable_task_status(
235 buffer_id,
236 buffer_row,
237 RunnableTaskStatus::from(result),
238 cx,
239 );
240 })
241 .ok();
242 },
243 window,
244 cx,
245 );
246 } else {
247 workspace.schedule_resolved_task(
248 task_source_kind,
249 resolved_task,
250 false,
251 window,
252 cx,
253 );
254 }
255
256 Some(Task::ready(Ok(())))
257 })
258 }
259 CodeActionsItem::CodeAction { action, provider } => {
260 if code_lens::try_handle_client_command(&action, self, &workspace, window, cx) {
261 return Some(Task::ready(Ok(())));
262 }
263
264 let apply_code_action =
265 provider.apply_code_action(buffer, action, true, window, cx);
266 let workspace = workspace.downgrade();
267 Some(cx.spawn_in(window, async move |editor, cx| {
268 let project_transaction = apply_code_action.await?;
269 Self::open_project_transaction(
270 &editor,
271 workspace,
272 project_transaction,
273 title,
274 cx,
275 )
276 .await
277 }))
278 }
279 CodeActionsItem::DebugScenario(scenario) => {
280 let context = actions_menu.actions.context.into();
281
282 workspace.update(cx, |workspace, cx| {
283 dap::send_telemetry(&scenario, TelemetrySpawnLocation::Gutter, cx);
284 workspace.start_debug_session(
285 scenario,
286 context,
287 Some(buffer),
288 None,
289 window,
290 cx,
291 );
292 });
293 Some(Task::ready(Ok(())))
294 }
295 }
296 }
297
298 fn runnable_task_key_for_source(
299 &self,
300 source: &Option<CodeActionSource>,
301 window: &mut Window,
302 cx: &mut Context<Self>,
303 ) -> Option<(BufferId, BufferRow)> {
304 let display_row = match source {
305 Some(CodeActionSource::RunMenu(row)) => *row,
306 _ => return None,
307 };
308 let snapshot = self.snapshot(window, cx);
309 let multibuffer_row =
310 MultiBufferRow(DisplayPoint::new(display_row, 0).to_point(&snapshot).row);
311 let (buffer_snapshot, range) = snapshot
312 .buffer_snapshot()
313 .buffer_line_for_row(multibuffer_row)?;
314 Some((buffer_snapshot.remote_id(), range.start.row))
315 }
316
317 pub fn code_actions_enabled_for_toolbar(&self, cx: &App) -> bool {
318 !self.code_action_providers.is_empty()
319 && EditorSettings::get_global(cx).toolbar.code_actions
320 }
321
322 pub fn has_available_code_actions_for_selection(&self) -> bool {
323 if let CodeActionsForSelection::Ready(ready) = &self.code_actions_for_selection {
324 !ready.actions.is_empty()
325 } else {
326 false
327 }
328 }
329
330 pub fn context_menu(&self) -> &RefCell<Option<CodeContextMenu>> {
331 &self.context_menu
332 }
333
334 pub(super) fn render_inline_code_actions(
335 &self,
336 icon_size: ui::IconSize,
337 display_row: DisplayRow,
338 is_active: bool,
339 cx: &mut Context<Self>,
340 ) -> AnyElement {
341 let show_tooltip = !self.context_menu_visible();
342 IconButton::new("inline_code_actions", ui::IconName::BoltFilled)
343 .icon_size(icon_size)
344 .shape(ui::IconButtonShape::Square)
345 .icon_color(ui::Color::Hidden)
346 .toggle_state(is_active)
347 .when(show_tooltip, |this| {
348 this.tooltip({
349 let focus_handle = self.focus_handle.clone();
350 move |_window, cx| {
351 Tooltip::for_action_in(
352 "Toggle Code Actions",
353 &ToggleCodeActions {
354 deployed_from: None,
355 quick_launch: false,
356 },
357 &focus_handle,
358 cx,
359 )
360 }
361 })
362 })
363 .on_click(cx.listener(move |editor, _: &ClickEvent, window, cx| {
364 window.focus(&editor.focus_handle(cx), cx);
365 editor.toggle_code_actions(
366 &crate::actions::ToggleCodeActions {
367 deployed_from: Some(crate::actions::CodeActionSource::Indicator(
368 display_row,
369 )),
370 quick_launch: false,
371 },
372 window,
373 cx,
374 );
375 }))
376 .into_any_element()
377 }
378
379 pub(super) fn refresh_code_actions_for_selection(
380 &mut self,
381 window: &mut Window,
382 cx: &mut Context<Self>,
383 ) {
384 self.code_actions_for_selection = CodeActionsForSelection::Fetching(
385 cx.spawn_in(window, async move |editor, cx| {
386 cx.background_executor()
387 .timer(CODE_ACTIONS_DEBOUNCE_TIMEOUT)
388 .await;
389
390 let (start_buffer, start, _, end, _newest_selection) = editor
391 .update(cx, |editor, cx| {
392 let newest_selection = editor.selections.newest_anchor().clone();
393 if newest_selection.head().diff_base_anchor().is_some() {
394 return None;
395 }
396 let display_snapshot = editor.display_snapshot(cx);
397 let newest_selection_adjusted =
398 editor.selections.newest_adjusted(&display_snapshot);
399 let buffer = editor.buffer.read(cx);
400
401 let (start_buffer, start) =
402 buffer.text_anchor_for_position(newest_selection_adjusted.start, cx)?;
403 let (end_buffer, end) =
404 buffer.text_anchor_for_position(newest_selection_adjusted.end, cx)?;
405
406 Some((start_buffer, start, end_buffer, end, newest_selection))
407 })
408 .ok()
409 .flatten()
410 .filter(|(start_buffer, _, end_buffer, _, _)| start_buffer == end_buffer)?;
411
412 let (providers, tasks) = editor
413 .update_in(cx, |editor, window, cx| {
414 let providers = editor.code_action_providers.clone();
415 let tasks = editor
416 .code_action_providers
417 .iter()
418 .map(|provider| {
419 provider.code_actions(&start_buffer, start..end, window, cx)
420 })
421 .collect::<Vec<_>>();
422 (providers, tasks)
423 })
424 .ok()?;
425
426 let mut actions = Vec::new();
427 for (provider, provider_actions) in
428 providers.into_iter().zip(future::join_all(tasks).await)
429 {
430 if let Some(provider_actions) = provider_actions.log_err() {
431 actions.extend(provider_actions.into_iter().map(|action| {
432 AvailableCodeAction {
433 action,
434 provider: provider.clone(),
435 }
436 }));
437 }
438 }
439
440 editor
441 .update(cx, |editor, cx| {
442 let new_actions = if actions.is_empty() {
443 editor.code_actions_for_selection = CodeActionsForSelection::None;
444 None
445 } else {
446 let new_actions = ActionFetchReady {
447 location: Location {
448 buffer: start_buffer,
449 range: start..end,
450 },
451 actions: Rc::from(actions),
452 };
453 editor.code_actions_for_selection =
454 CodeActionsForSelection::Ready(new_actions.clone());
455 Some(new_actions)
456 };
457 cx.notify();
458 new_actions
459 })
460 .ok()
461 .flatten()
462 })
463 .shared(),
464 );
465 }
466
467 fn debug_scenarios(
468 &mut self,
469 resolved_tasks: &Option<ResolvedTasks>,
470 buffer: &Entity<Buffer>,
471 cx: &mut App,
472 ) -> Task<Vec<task::DebugScenario>> {
473 maybe!({
474 let project = self.project()?;
475 let dap_store = project.read(cx).dap_store();
476 let mut scenarios = vec![];
477 let resolved_tasks = resolved_tasks.as_ref()?;
478 let buffer = buffer.read(cx);
479 let language = buffer.language()?;
480 let debug_adapter = LanguageSettings::for_buffer(&buffer, cx)
481 .debuggers
482 .first()
483 .map(SharedString::from)
484 .or_else(|| language.config().debuggers.first().map(SharedString::from))?;
485
486 dap_store.update(cx, |dap_store, cx| {
487 for (_, task) in &resolved_tasks.templates {
488 let maybe_scenario = dap_store.debug_scenario_for_build_task(
489 task.original_task().clone(),
490 debug_adapter.clone().into(),
491 task.display_label().to_owned().into(),
492 cx,
493 );
494 scenarios.push(maybe_scenario);
495 }
496 });
497 Some(cx.background_spawn(async move {
498 futures::future::join_all(scenarios)
499 .await
500 .into_iter()
501 .flatten()
502 .collect::<Vec<_>>()
503 }))
504 })
505 .unwrap_or_else(|| Task::ready(vec![]))
506 }
507}
508
509pub trait CodeActionProvider {
510 fn id(&self) -> Arc<str>;
511
512 fn code_actions(
513 &self,
514 buffer: &Entity<Buffer>,
515 range: Range<text::Anchor>,
516 window: &mut Window,
517 cx: &mut App,
518 ) -> Task<Result<Vec<CodeAction>>>;
519
520 fn apply_code_action(
521 &self,
522 buffer_handle: Entity<Buffer>,
523 action: CodeAction,
524 push_to_history: bool,
525 window: &mut Window,
526 cx: &mut App,
527 ) -> Task<Result<ProjectTransaction>>;
528}
529
530impl CodeActionProvider for Entity<Project> {
531 fn id(&self) -> Arc<str> {
532 "project".into()
533 }
534
535 fn code_actions(
536 &self,
537 buffer: &Entity<Buffer>,
538 range: Range<text::Anchor>,
539 _window: &mut Window,
540 cx: &mut App,
541 ) -> Task<Result<Vec<CodeAction>>> {
542 self.update(cx, |project, cx| {
543 let code_lens_actions = if EditorSettings::get_global(cx).code_lens.show_in_menu() {
544 Some(project.code_lens_actions(buffer, range.clone(), cx))
545 } else {
546 None
547 };
548 let code_actions = project.code_actions(buffer, range, None, cx);
549 cx.background_spawn(async move {
550 let code_lens_actions = match code_lens_actions {
551 Some(task) => task.await.context("code lens fetch")?.unwrap_or_default(),
552 None => Vec::new(),
553 };
554 let code_actions = code_actions
555 .await
556 .context("code action fetch")?
557 .unwrap_or_default();
558 Ok(code_lens_actions.into_iter().chain(code_actions).collect())
559 })
560 })
561 }
562
563 fn apply_code_action(
564 &self,
565 buffer_handle: Entity<Buffer>,
566 action: CodeAction,
567 push_to_history: bool,
568 _window: &mut Window,
569 cx: &mut App,
570 ) -> Task<Result<ProjectTransaction>> {
571 self.update(cx, |project, cx| {
572 project.apply_code_action(buffer_handle, action, push_to_history, cx)
573 })
574 }
575}
576