diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 0ff1dd7842..28590eb8e2 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -466,6 +466,32 @@ enum ContextMenu { Completion(CompletionMenu), } +impl ContextMenu { + fn select_prev(&mut self, cx: &mut ViewContext) { + match self { + ContextMenu::Completion(menu) => menu.select_prev(cx), + } + } + + fn select_next(&mut self, cx: &mut ViewContext) { + 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) { + 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) { + 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::( + 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) { 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 { 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 { - 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::( - 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 { + 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) { - 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) { - 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); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index a22e6021e0..8452989153 100644 --- a/crates/editor/src/element.rs +++ b/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::(&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)); } } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index fb5958fcd4..d3e41f0629 100644 --- a/crates/language/src/buffer.rs +++ b/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}; diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 1d9f784f47..d50ca484e2 100644 --- a/crates/server/src/rpc.rs +++ b/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() }"); });