Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:31:38.823Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

code_actions.rs

576 lines · 22.8 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.