Move rendering and select prev/next into ContextMenu enum

Nathan Sobo created

This prepares the way to have a code actions context menu.

Change summary

crates/editor/src/editor.rs   | 266 ++++++++++++++++++++----------------
crates/editor/src/element.rs  |   4 
crates/language/src/buffer.rs |   2 
crates/server/src/rpc.rs      |   2 
4 files changed, 149 insertions(+), 125 deletions(-)

Detailed changes

crates/editor/src/editor.rs 🔗

@@ -466,6 +466,32 @@ enum ContextMenu {
     Completion(CompletionMenu),
 }
 
+impl ContextMenu {
+    fn select_prev(&mut self, cx: &mut ViewContext<Editor>) {
+        match self {
+            ContextMenu::Completion(menu) => menu.select_prev(cx),
+        }
+    }
+
+    fn select_next(&mut self, cx: &mut ViewContext<Editor>) {
+        match self {
+            ContextMenu::Completion(menu) => menu.select_next(cx),
+        }
+    }
+
+    fn should_render(&self) -> bool {
+        match self {
+            ContextMenu::Completion(menu) => menu.should_render(),
+        }
+    }
+
+    fn render(&self, build_settings: BuildSettings, cx: &AppContext) -> ElementBox {
+        match self {
+            ContextMenu::Completion(menu) => menu.render(build_settings, cx),
+        }
+    }
+}
+
 struct CompletionMenu {
     id: CompletionId,
     initial_position: Anchor,
@@ -477,6 +503,95 @@ struct CompletionMenu {
 }
 
 impl CompletionMenu {
+    fn select_prev(&mut self, cx: &mut ViewContext<Editor>) {
+        if self.selected_item > 0 {
+            self.selected_item -= 1;
+            self.list.scroll_to(ScrollTarget::Show(self.selected_item));
+        }
+        cx.notify();
+    }
+
+    fn select_next(&mut self, cx: &mut ViewContext<Editor>) {
+        if self.selected_item + 1 < self.matches.len() {
+            self.selected_item += 1;
+            self.list.scroll_to(ScrollTarget::Show(self.selected_item));
+        }
+        cx.notify();
+    }
+
+    fn should_render(&self) -> bool {
+        !self.matches.is_empty()
+    }
+
+    fn render(&self, build_settings: BuildSettings, cx: &AppContext) -> ElementBox {
+        enum CompletionTag {}
+
+        let settings = build_settings(cx);
+        let completions = self.completions.clone();
+        let matches = self.matches.clone();
+        let selected_item = self.selected_item;
+        UniformList::new(self.list.clone(), matches.len(), move |range, items, cx| {
+            let settings = build_settings(cx);
+            let start_ix = range.start;
+            for (ix, mat) in matches[range].iter().enumerate() {
+                let completion = &completions[mat.candidate_id];
+                let item_ix = start_ix + ix;
+                items.push(
+                    MouseEventHandler::new::<CompletionTag, _, _, _>(
+                        mat.candidate_id,
+                        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(completion.label.text.clone(), settings.style.text.clone())
+                                .with_soft_wrap(false)
+                                .with_highlights(combine_syntax_and_fuzzy_match_highlights(
+                                    &completion.label.text,
+                                    settings.style.text.color.into(),
+                                    styled_runs_for_completion_label(
+                                        &completion.label,
+                                        settings.style.text.color,
+                                        &settings.style.syntax,
+                                    ),
+                                    &mat.positions,
+                                ))
+                                .contained()
+                                .with_style(item_style)
+                                .boxed()
+                        },
+                    )
+                    .with_cursor_style(CursorStyle::PointingHand)
+                    .on_mouse_down(move |cx| {
+                        cx.dispatch_action(ConfirmCompletion(Some(item_ix)));
+                    })
+                    .boxed(),
+                );
+            }
+        })
+        .with_width_from_item(
+            self.matches
+                .iter()
+                .enumerate()
+                .max_by_key(|(_, mat)| {
+                    self.completions[mat.candidate_id]
+                        .label
+                        .text
+                        .chars()
+                        .count()
+                })
+                .map(|(ix, _)| ix),
+        )
+        .contained()
+        .with_style(settings.style.autocomplete.container)
+        .boxed()
+    }
+
     pub async fn filter(&mut self, query: Option<&str>, executor: Arc<executor::Background>) {
         let mut matches = if let Some(query) = query {
             fuzzy::match_strings(
@@ -1648,22 +1763,6 @@ impl Editor {
         }
     }
 
-    fn completion_menu(&self) -> Option<&CompletionMenu> {
-        if let Some(ContextMenu::Completion(menu)) = self.context_menu.as_ref() {
-            Some(menu)
-        } else {
-            None
-        }
-    }
-
-    fn completion_menu_mut(&mut self) -> Option<&mut CompletionMenu> {
-        if let Some(ContextMenu::Completion(menu)) = self.context_menu.as_mut() {
-            Some(menu)
-        } else {
-            None
-        }
-    }
-
     fn completion_query(buffer: &MultiBufferSnapshot, position: impl ToOffset) -> Option<String> {
         let offset = position.to_offset(buffer);
         let (word_range, kind) = buffer.surrounding_word(offset);
@@ -1718,10 +1817,14 @@ impl Editor {
 
                 if let Some(this) = cx.read(|cx| this.upgrade(cx)) {
                     this.update(&mut cx, |this, cx| {
-                        if let Some(prev_menu) = this.completion_menu() {
-                            if prev_menu.id > menu.id {
-                                return;
+                        match this.context_menu.as_ref() {
+                            None => {}
+                            Some(ContextMenu::Completion(prev_menu)) => {
+                                if prev_menu.id > menu.id {
+                                    return;
+                                }
                             }
+                            _ => return,
                         }
 
                         this.completion_tasks.retain(|(id, _)| *id > menu.id);
@@ -1847,89 +1950,16 @@ impl Editor {
         }))
     }
 
-    pub fn has_completions(&self) -> bool {
-        self.completion_menu()
-            .map_or(false, |c| !c.matches.is_empty())
+    pub fn should_render_context_menu(&self) -> bool {
+        self.context_menu
+            .as_ref()
+            .map_or(false, |menu| menu.should_render())
     }
 
-    pub fn render_completions(&self, cx: &AppContext) -> Option<ElementBox> {
-        enum CompletionTag {}
-
-        self.completion_menu().map(|state| {
-            let build_settings = self.build_settings.clone();
-            let settings = build_settings(cx);
-            let completions = state.completions.clone();
-            let matches = state.matches.clone();
-            let selected_item = state.selected_item;
-            UniformList::new(
-                state.list.clone(),
-                matches.len(),
-                move |range, items, cx| {
-                    let settings = build_settings(cx);
-                    let start_ix = range.start;
-                    for (ix, mat) in matches[range].iter().enumerate() {
-                        let completion = &completions[mat.candidate_id];
-                        let item_ix = start_ix + ix;
-                        items.push(
-                            MouseEventHandler::new::<CompletionTag, _, _, _>(
-                                mat.candidate_id,
-                                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(
-                                        completion.label.text.clone(),
-                                        settings.style.text.clone(),
-                                    )
-                                    .with_soft_wrap(false)
-                                    .with_highlights(combine_syntax_and_fuzzy_match_highlights(
-                                        &completion.label.text,
-                                        settings.style.text.color.into(),
-                                        styled_runs_for_completion_label(
-                                            &completion.label,
-                                            settings.style.text.color,
-                                            &settings.style.syntax,
-                                        ),
-                                        &mat.positions,
-                                    ))
-                                    .contained()
-                                    .with_style(item_style)
-                                    .boxed()
-                                },
-                            )
-                            .with_cursor_style(CursorStyle::PointingHand)
-                            .on_mouse_down(move |cx| {
-                                cx.dispatch_action(ConfirmCompletion(Some(item_ix)));
-                            })
-                            .boxed(),
-                        );
-                    }
-                },
-            )
-            .with_width_from_item(
-                state
-                    .matches
-                    .iter()
-                    .enumerate()
-                    .max_by_key(|(_, mat)| {
-                        state.completions[mat.candidate_id]
-                            .label
-                            .text
-                            .chars()
-                            .count()
-                    })
-                    .map(|(ix, _)| ix),
-            )
-            .contained()
-            .with_style(settings.style.autocomplete.container)
-            .boxed()
-        })
+    pub fn render_context_menu(&self, cx: &AppContext) -> Option<ElementBox> {
+        self.context_menu
+            .as_ref()
+            .map(|menu| menu.render(self.build_settings.clone(), cx))
     }
 
     pub fn insert_snippet(
@@ -2737,14 +2767,8 @@ impl Editor {
     }
 
     pub fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext<Self>) {
-        if let Some(context_menu) = self.completion_menu_mut() {
-            if context_menu.selected_item > 0 {
-                context_menu.selected_item -= 1;
-                context_menu
-                    .list
-                    .scroll_to(ScrollTarget::Show(context_menu.selected_item));
-            }
-            cx.notify();
+        if let Some(context_menu) = self.context_menu.as_mut() {
+            context_menu.select_prev(cx);
             return;
         }
 
@@ -2786,14 +2810,8 @@ impl Editor {
     }
 
     pub fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext<Self>) {
-        if let Some(context_menu) = &mut self.completion_menu_mut() {
-            if context_menu.selected_item + 1 < context_menu.matches.len() {
-                context_menu.selected_item += 1;
-                context_menu
-                    .list
-                    .scroll_to(ScrollTarget::Show(context_menu.selected_item));
-            }
-            cx.notify();
+        if let Some(context_menu) = self.context_menu.as_mut() {
+            context_menu.select_next(cx);
             return;
         }
 
@@ -4087,16 +4105,22 @@ impl Editor {
             }
         }
 
-        if let Some((context_menu, cursor_position)) =
-            self.completion_menu_mut().zip(new_cursor_position)
-        {
+        let completion_menu =
+            if let Some(ContextMenu::Completion(menu)) = self.context_menu.as_mut() {
+                Some(menu)
+            } else {
+                None
+            };
+
+        if let Some((completion_menu, cursor_position)) = completion_menu.zip(new_cursor_position) {
             let cursor_position = cursor_position.to_offset(&buffer);
-            let (word_range, kind) = buffer.surrounding_word(context_menu.initial_position.clone());
+            let (word_range, kind) =
+                buffer.surrounding_word(completion_menu.initial_position.clone());
             if kind == Some(CharKind::Word) && word_range.to_inclusive().contains(&cursor_position)
             {
                 let query = Self::completion_query(&buffer, cursor_position);
                 cx.background()
-                    .block(context_menu.filter(query.as_deref(), cx.background().clone()));
+                    .block(completion_menu.filter(query.as_deref(), cx.background().clone()));
                 self.show_completions(&ShowCompletions, cx);
             } else {
                 self.hide_completions(cx);

crates/editor/src/element.rs 🔗

@@ -880,14 +880,14 @@ impl Element for EditorElement {
                 snapshot = view.snapshot(cx);
             }
 
-            if view.has_completions() {
+            if view.should_render_context_menu() {
                 let newest_selection_head = view
                     .newest_selection::<usize>(&snapshot.buffer_snapshot)
                     .head()
                     .to_display_point(&snapshot);
 
                 if (start_row..end_row).contains(&newest_selection_head.row()) {
-                    let list = view.render_completions(cx).unwrap();
+                    let list = view.render_context_menu(cx).unwrap();
                     completions = Some((newest_selection_head, list));
                 }
             }

crates/language/src/buffer.rs 🔗

@@ -14,7 +14,7 @@ use clock::ReplicaId;
 use futures::FutureExt as _;
 use gpui::{AppContext, Entity, ModelContext, MutableAppContext, Task};
 use lazy_static::lazy_static;
-use lsp::{CodeActionKind, LanguageServer};
+use lsp::LanguageServer;
 use parking_lot::Mutex;
 use postage::{prelude::Stream, sink::Sink, watch};
 use similar::{ChangeTag, TextDiff};

crates/server/src/rpc.rs 🔗

@@ -2468,7 +2468,7 @@ mod tests {
         // Confirm a completion on the guest.
         editor_b.next_notification(&cx_b).await;
         editor_b.update(&mut cx_b, |editor, cx| {
-            assert!(editor.has_completions());
+            assert!(editor.should_render_context_menu());
             editor.confirm_completion(Some(0), cx);
             assert_eq!(editor.text(cx), "fn main() { a.first_method() }");
         });