use std::rc::Rc; use gpui::prelude::FluentBuilder; use gpui::{ Action, AnyElement, App, AppContext, Context, DismissEvent, Empty, Entity, EventEmitter, InteractiveElement as _, IntoElement, ParentElement, Pixels, Point, Render, RenderOnce, SharedString, Styled, StyledText, Subscription, Window, deferred, div, px, relative, }; use lsp_types::CodeAction; use theme::ActiveTheme; const MAX_MENU_WIDTH: Pixels = px(320.); const MAX_MENU_HEIGHT: Pixels = px(480.); use crate::input::popovers::editor_popover; use crate::input::{self, InputState}; use crate::list::{List, ListDelegate, ListEvent, ListState}; use crate::{IndexPath, Selectable, actions, h_flex}; #[derive(Debug, Clone)] pub(crate) struct CodeActionItem { /// The `id` of the `CodeActionProvider` that provided this item. pub(crate) provider_id: SharedString, pub(crate) action: CodeAction, } struct MenuDelegate { menu: Entity, items: Vec>, selected_ix: usize, } impl MenuDelegate { fn set_items(&mut self, items: Vec) { self.items = items.into_iter().map(Rc::new).collect(); self.selected_ix = 0; } fn selected_item(&self) -> Option<&Rc> { self.items.get(self.selected_ix) } } #[derive(IntoElement)] struct MenuItem { ix: usize, item: Rc, children: Vec, selected: bool, } impl MenuItem { fn new(ix: usize, item: Rc) -> Self { Self { ix, item, children: vec![], selected: false, } } } impl Selectable for MenuItem { fn selected(mut self, selected: bool) -> Self { self.selected = selected; self } fn is_selected(&self) -> bool { self.selected } } impl ParentElement for MenuItem { fn extend(&mut self, elements: impl IntoIterator) { self.children.extend(elements); } } impl RenderOnce for MenuItem { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { let item = self.item; let highlights = vec![]; h_flex() .id(self.ix) .gap_2() .p_1() .text_xs() .line_height(relative(1.)) .rounded(cx.theme().radius) .hover(|this| this.bg(cx.theme().secondary_hover)) .when(self.selected, |this| { this.bg(cx.theme().secondary_background) .text_color(cx.theme().secondary_foreground) }) .child( div().child(StyledText::new(item.action.title.clone()).with_highlights(highlights)), ) .children(self.children) } } impl EventEmitter for MenuDelegate {} impl ListDelegate for MenuDelegate { type Item = MenuItem; fn items_count(&self, _: usize, _: &gpui::App) -> usize { self.items.len() } fn render_item( &mut self, ix: crate::IndexPath, _: &mut Window, _: &mut Context>, ) -> Option { let item = self.items.get(ix.row)?; Some(MenuItem::new(ix.row, item.clone())) } fn set_selected_index( &mut self, ix: Option, _: &mut Window, cx: &mut Context>, ) { self.selected_ix = ix.map(|i| i.row).unwrap_or(0); cx.notify(); } fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context>) { let Some(item) = self.selected_item() else { return; }; self.menu.update(cx, |this, cx| { this.select_item(&item, window, cx); }); } } /// A context menu for code completions and code actions. pub struct CodeActionMenu { offset: usize, state: Entity, list: Entity>, open: bool, _subscriptions: Vec, } impl CodeActionMenu { /// Creates a new `CompletionMenu` with the given offset and completion items. /// /// NOTE: This element should not call from InputState::new, unless that will stack overflow. pub(crate) fn new( state: Entity, window: &mut Window, cx: &mut App, ) -> Entity { cx.new(|cx| { let view = cx.entity(); let menu = MenuDelegate { menu: view, items: vec![], selected_ix: 0, }; let list = cx.new(|cx| ListState::new(menu, window, cx)); let _subscriptions = vec![ cx.subscribe(&list, |this: &mut Self, _, ev: &ListEvent, cx| { match ev { ListEvent::Confirm(_) => { this.hide(cx); } _ => {} } cx.notify(); }), ]; Self { offset: 0, state, list, open: false, _subscriptions, } }) } fn select_item(&mut self, item: &CodeActionItem, window: &mut Window, cx: &mut Context) { let state = self.state.clone(); let item = item.clone(); cx.spawn_in(window, { async move |_, cx| { state.update_in(cx, |state, window, cx| { state.perform_code_action(&item, window, cx); }) } }) .detach(); self.hide(cx); } pub(crate) fn handle_action( &mut self, action: Box, window: &mut Window, cx: &mut Context, ) -> bool { if !self.open { return false; } cx.propagate(); if input::Enter::is_primary(&*action) { self.on_action_enter(window, cx); } else if action.partial_eq(&input::Escape) { self.on_action_escape(window, cx); } else if action.partial_eq(&input::MoveUp) { self.on_action_up(window, cx); } else if action.partial_eq(&input::MoveDown) { self.on_action_down(window, cx); } else { return false; } true } fn on_action_enter(&mut self, window: &mut Window, cx: &mut Context) { let Some(item) = self.list.read(cx).delegate().selected_item().cloned() else { return; }; self.select_item(&item, window, cx); } fn on_action_escape(&mut self, _: &mut Window, cx: &mut Context) { self.hide(cx); } fn on_action_up(&mut self, window: &mut Window, cx: &mut Context) { self.list.update(cx, |this, cx| { this.on_action_select_prev(&actions::SelectUp, window, cx) }); } fn on_action_down(&mut self, window: &mut Window, cx: &mut Context) { self.list.update(cx, |this, cx| { this.on_action_select_next(&actions::SelectDown, window, cx) }); } pub(crate) fn is_open(&self) -> bool { self.open } /// Hide the completion menu and reset the trigger start offset. pub(crate) fn hide(&mut self, cx: &mut Context) { self.open = false; cx.notify(); } pub(crate) fn show( &mut self, offset: usize, items: impl Into>, window: &mut Window, cx: &mut Context, ) { let items = items.into(); self.offset = offset; self.open = true; self.list.update(cx, |this, cx| { this.delegate_mut().set_items(items); this.set_selected_index(Some(IndexPath::new(0)), window, cx); }); cx.notify(); } fn origin(&self, cx: &App) -> Option> { let state = self.state.read(cx); let Some(last_layout) = state.last_layout.as_ref() else { return None; }; let Some(cursor_origin) = last_layout.cursor_bounds.map(|b| b.origin) else { return None; }; let scroll_origin = self.state.read(cx).scroll_handle.offset(); Some( scroll_origin + cursor_origin - state.input_bounds.origin + Point::new(-px(4.), last_layout.line_height + px(4.)), ) } } impl Render for CodeActionMenu { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { if !self.open { return Empty.into_any_element(); } if self.list.read(cx).delegate().items.is_empty() { self.open = false; return Empty.into_any_element(); } let Some(pos) = self.origin(cx) else { return Empty.into_any_element(); }; let max_width = MAX_MENU_WIDTH.min(window.bounds().size.width - pos.x); deferred( editor_popover("code-action-menu", cx) .absolute() .left(pos.x) .top(pos.y) .max_w(max_width) .min_w(px(120.)) .child(List::new(&self.list).max_h(MAX_MENU_HEIGHT)) .on_mouse_down_out(cx.listener(|this, _, _, cx| { this.hide(cx); })), ) .into_any_element() } }