Render code actions context menu

Nathan Sobo created

Change summary

crates/editor/src/editor.rs | 132 +++++++++++++++++++++++++++++++++-----
1 file changed, 112 insertions(+), 20 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -126,6 +126,7 @@ action!(Select, SelectPhase);
 action!(ShowCompletions);
 action!(ShowCodeActions);
 action!(ConfirmCompletion, Option<usize>);
+action!(ConfirmCodeAction, Option<usize>);
 
 pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec<Box<dyn PathOpener>>) {
     path_openers.push(Box::new(items::BufferOpener));
@@ -463,36 +464,41 @@ struct SnippetState {
 struct InvalidationStack<T>(Vec<T>);
 
 enum ContextMenu {
-    Completion(CompletionMenu),
+    Completions(CompletionsMenu),
+    CodeActions(CodeActionsMenu),
 }
 
 impl ContextMenu {
     fn select_prev(&mut self, cx: &mut ViewContext<Editor>) {
         match self {
-            ContextMenu::Completion(menu) => menu.select_prev(cx),
+            ContextMenu::Completions(menu) => menu.select_prev(cx),
+            ContextMenu::CodeActions(menu) => menu.select_prev(cx),
         }
     }
 
     fn select_next(&mut self, cx: &mut ViewContext<Editor>) {
         match self {
-            ContextMenu::Completion(menu) => menu.select_next(cx),
+            ContextMenu::Completions(menu) => menu.select_next(cx),
+            ContextMenu::CodeActions(menu) => menu.select_next(cx),
         }
     }
 
     fn should_render(&self) -> bool {
         match self {
-            ContextMenu::Completion(menu) => menu.should_render(),
+            ContextMenu::Completions(menu) => menu.should_render(),
+            ContextMenu::CodeActions(menu) => menu.should_render(),
         }
     }
 
     fn render(&self, build_settings: BuildSettings, cx: &AppContext) -> ElementBox {
         match self {
-            ContextMenu::Completion(menu) => menu.render(build_settings, cx),
+            ContextMenu::Completions(menu) => menu.render(build_settings, cx),
+            ContextMenu::CodeActions(menu) => menu.render(build_settings, cx),
         }
     }
 }
 
-struct CompletionMenu {
+struct CompletionsMenu {
     id: CompletionId,
     initial_position: Anchor,
     completions: Arc<[Completion<Anchor>]>,
@@ -502,7 +508,7 @@ struct CompletionMenu {
     list: UniformListState,
 }
 
-impl CompletionMenu {
+impl CompletionsMenu {
     fn select_prev(&mut self, cx: &mut ViewContext<Editor>) {
         if self.selected_item > 0 {
             self.selected_item -= 1;
@@ -633,6 +639,79 @@ impl CompletionMenu {
     }
 }
 
+struct CodeActionsMenu {
+    actions: Arc<[lsp::CodeAction]>,
+    selected_item: usize,
+    list: UniformListState,
+}
+
+impl CodeActionsMenu {
+    fn select_prev(&mut self, cx: &mut ViewContext<Editor>) {
+        if self.selected_item > 0 {
+            self.selected_item -= 1;
+            cx.notify()
+        }
+    }
+
+    fn select_next(&mut self, cx: &mut ViewContext<Editor>) {
+        if self.selected_item + 1 < self.actions.len() {
+            self.selected_item += 1;
+            cx.notify()
+        }
+    }
+
+    fn should_render(&self) -> bool {
+        !self.actions.is_empty()
+    }
+
+    fn render(&self, build_settings: BuildSettings, cx: &AppContext) -> ElementBox {
+        enum ActionTag {}
+
+        let settings = build_settings(cx);
+        let actions = self.actions.clone();
+        let selected_item = self.selected_item;
+        UniformList::new(self.list.clone(), actions.len(), move |range, items, cx| {
+            let settings = build_settings(cx);
+            let start_ix = range.start;
+            for (ix, action) in actions[range].iter().enumerate() {
+                let item_ix = start_ix + ix;
+                items.push(
+                    MouseEventHandler::new::<ActionTag, _, _, _>(item_ix, cx, |state, _| {
+                        let item_style = if item_ix == selected_item {
+                            settings.style.autocomplete.selected_item
+                        } else if state.hovered {
+                            settings.style.autocomplete.hovered_item
+                        } else {
+                            settings.style.autocomplete.item
+                        };
+
+                        Text::new(action.title.clone(), settings.style.text.clone())
+                            .with_soft_wrap(false)
+                            .contained()
+                            .with_style(item_style)
+                            .boxed()
+                    })
+                    .with_cursor_style(CursorStyle::PointingHand)
+                    .on_mouse_down(move |cx| {
+                        cx.dispatch_action(ConfirmCodeAction(Some(item_ix)));
+                    })
+                    .boxed(),
+                );
+            }
+        })
+        .with_width_from_item(
+            self.actions
+                .iter()
+                .enumerate()
+                .max_by_key(|(_, action)| action.title.chars().count())
+                .map(|(ix, _)| ix),
+        )
+        .contained()
+        .with_style(settings.style.autocomplete.container)
+        .boxed()
+    }
+}
+
 #[derive(Debug)]
 struct ActiveDiagnosticGroup {
     primary_range: Range<Anchor>,
@@ -1794,7 +1873,7 @@ impl Editor {
             async move {
                 let completions = completions.await?;
 
-                let mut menu = CompletionMenu {
+                let mut menu = CompletionsMenu {
                     id,
                     initial_position: position,
                     match_candidates: completions
@@ -1819,7 +1898,7 @@ impl Editor {
                     this.update(&mut cx, |this, cx| {
                         match this.context_menu.as_ref() {
                             None => {}
-                            Some(ContextMenu::Completion(prev_menu)) => {
+                            Some(ContextMenu::Completions(prev_menu)) => {
                                 if prev_menu.id > menu.id {
                                     return;
                                 }
@@ -1831,7 +1910,7 @@ impl Editor {
                         if menu.matches.is_empty() {
                             this.hide_completions(cx);
                         } else if this.focused {
-                            this.context_menu = Some(ContextMenu::Completion(menu));
+                            this.context_menu = Some(ContextMenu::Completions(menu));
                         }
 
                         cx.notify();
@@ -1854,17 +1933,29 @@ impl Editor {
         let actions = self
             .buffer
             .update(cx, |buffer, cx| buffer.code_actions(position.clone(), cx));
-        cx.spawn(|this, cx| async move {
-            dbg!(actions.await.unwrap());
+
+        cx.spawn(|this, mut cx| async move {
+            let actions = actions.await?;
+            if !actions.is_empty() {
+                this.update(&mut cx, |this, cx| {
+                    this.context_menu = Some(ContextMenu::CodeActions(CodeActionsMenu {
+                        actions: actions.into(),
+                        selected_item: 0,
+                        list: UniformListState::default(),
+                    }));
+                    cx.notify();
+                });
+            }
+            Ok::<_, anyhow::Error>(())
         })
-        .detach();
+        .detach_and_log_err(cx);
     }
 
-    fn hide_completions(&mut self, cx: &mut ViewContext<Self>) -> Option<CompletionMenu> {
+    fn hide_completions(&mut self, cx: &mut ViewContext<Self>) -> Option<CompletionsMenu> {
         cx.notify();
         self.completion_tasks.clear();
         self.context_menu.take().and_then(|menu| {
-            if let ContextMenu::Completion(menu) = menu {
+            if let ContextMenu::Completions(menu) = menu {
                 Some(menu)
             } else {
                 None
@@ -4105,12 +4196,13 @@ impl Editor {
             }
         }
 
-        let completion_menu =
-            if let Some(ContextMenu::Completion(menu)) = self.context_menu.as_mut() {
-                Some(menu)
-            } else {
+        let completion_menu = match self.context_menu.as_mut() {
+            Some(ContextMenu::Completions(menu)) => Some(menu),
+            _ => {
+                self.context_menu.take();
                 None
-            };
+            }
+        };
 
         if let Some((completion_menu, cursor_position)) = completion_menu.zip(new_cursor_position) {
             let cursor_position = cursor_position.to_offset(&buffer);