diff options
author | Blaž Hrastnik | 2021-05-22 08:33:42 +0000 |
---|---|---|
committer | Blaž Hrastnik | 2021-05-22 08:33:42 +0000 |
commit | b114cfa119bc94396f1ed38109a51183035574ed (patch) | |
tree | 33e30a93e984e41ae0f507478ce93754261a331a /helix-term/src | |
parent | f1dc25a7741ebec630a09eaf7c7c3c91947f5078 (diff) |
Display more data in completion popups.
Diffstat (limited to 'helix-term/src')
-rw-r--r-- | helix-term/src/ui/completion.rs | 203 | ||||
-rw-r--r-- | helix-term/src/ui/menu.rs | 56 |
2 files changed, 157 insertions, 102 deletions
diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 719daa0f..f79c2235 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -12,11 +12,63 @@ use helix_core::{Position, Transaction}; use helix_view::Editor; use crate::commands; -use crate::ui::{Markdown, Menu, Popup, PromptEvent}; +use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent}; use helix_lsp::lsp; use lsp::CompletionItem; +impl menu::Item for CompletionItem { + fn filter_text(&self) -> &str { + self.filter_text + .as_ref() + .unwrap_or_else(|| &self.label) + .as_str() + } + + fn label(&self) -> &str { + self.label.as_str() + } + + fn row(&self) -> menu::Row { + menu::Row::new(vec![ + menu::Cell::from(self.label.as_str()), + menu::Cell::from(match self.kind { + Some(lsp::CompletionItemKind::Text) => "text", + Some(lsp::CompletionItemKind::Method) => "method", + Some(lsp::CompletionItemKind::Function) => "function", + Some(lsp::CompletionItemKind::Constructor) => "constructor", + Some(lsp::CompletionItemKind::Field) => "field", + Some(lsp::CompletionItemKind::Variable) => "variable", + Some(lsp::CompletionItemKind::Class) => "class", + Some(lsp::CompletionItemKind::Interface) => "interface", + Some(lsp::CompletionItemKind::Module) => "module", + Some(lsp::CompletionItemKind::Property) => "property", + Some(lsp::CompletionItemKind::Unit) => "unit", + Some(lsp::CompletionItemKind::Value) => "value", + Some(lsp::CompletionItemKind::Enum) => "enum", + Some(lsp::CompletionItemKind::Keyword) => "keyword", + Some(lsp::CompletionItemKind::Snippet) => "snippet", + Some(lsp::CompletionItemKind::Color) => "color", + Some(lsp::CompletionItemKind::File) => "file", + Some(lsp::CompletionItemKind::Reference) => "reference", + Some(lsp::CompletionItemKind::Folder) => "folder", + Some(lsp::CompletionItemKind::EnumMember) => "enum_member", + Some(lsp::CompletionItemKind::Constant) => "constant", + Some(lsp::CompletionItemKind::Struct) => "struct", + Some(lsp::CompletionItemKind::Event) => "event", + Some(lsp::CompletionItemKind::Operator) => "operator", + Some(lsp::CompletionItemKind::TypeParameter) => "type_param", + None => "", + }), + // self.detail.as_deref().unwrap_or("") + // self.label_details + // .as_ref() + // .or(self.detail()) + // .as_str(), + ]) + } +} + /// Wraps a Menu. pub struct Completion { popup: Popup<Menu<CompletionItem>>, // TODO: Popup<Menu> need to be able to access contents. @@ -31,92 +83,83 @@ impl Completion { trigger_offset: usize, ) -> Self { // let items: Vec<CompletionItem> = Vec::new(); - let mut menu = Menu::new( - items, - |item| { - // format_fn - item.label.as_str().into() - - // TODO: use item.filter_text for filtering - }, - move |editor: &mut Editor, item, event| { - match event { - PromptEvent::Abort => { - // revert state - // let id = editor.view().doc; - // let doc = &mut editor.documents[id]; - // doc.state = snapshot.clone(); - } - PromptEvent::Validate => { - let (view, doc) = editor.current(); - - // revert state to what it was before the last update - // doc.state = snapshot.clone(); - - // extract as fn(doc, item): - - // TODO: need to apply without composing state... - // TODO: need to update lsp on accept/cancel by diffing the snapshot with - // the final state? - // -> on update simply update the snapshot, then on accept redo the call, - // finally updating doc.changes + notifying lsp. - // - // or we could simply use doc.undo + apply when changing between options - - // always present here - let item = item.unwrap(); - - use helix_lsp::{lsp, util}; - // determine what to insert: text_edit | insert_text | label - let edit = if let Some(edit) = &item.text_edit { - match edit { - lsp::CompletionTextEdit::Edit(edit) => edit.clone(), - lsp::CompletionTextEdit::InsertAndReplace(item) => { - unimplemented!("completion: insert_and_replace {:?}", item) - } + let mut menu = Menu::new(items, move |editor: &mut Editor, item, event| { + match event { + PromptEvent::Abort => { + // revert state + // let id = editor.view().doc; + // let doc = &mut editor.documents[id]; + // doc.state = snapshot.clone(); + } + PromptEvent::Validate => { + let (view, doc) = editor.current(); + + // revert state to what it was before the last update + // doc.state = snapshot.clone(); + + // extract as fn(doc, item): + + // TODO: need to apply without composing state... + // TODO: need to update lsp on accept/cancel by diffing the snapshot with + // the final state? + // -> on update simply update the snapshot, then on accept redo the call, + // finally updating doc.changes + notifying lsp. + // + // or we could simply use doc.undo + apply when changing between options + + // always present here + let item = item.unwrap(); + + use helix_lsp::{lsp, util}; + // determine what to insert: text_edit | insert_text | label + let edit = if let Some(edit) = &item.text_edit { + match edit { + lsp::CompletionTextEdit::Edit(edit) => edit.clone(), + lsp::CompletionTextEdit::InsertAndReplace(item) => { + unimplemented!("completion: insert_and_replace {:?}", item) } - } else { - item.insert_text.as_ref().unwrap_or(&item.label); - unimplemented!(); - // lsp::TextEdit::new(); TODO: calculate a TextEdit from insert_text - // and we insert at position. - }; - - // if more text was entered, remove it - let cursor = doc.selection(view.id).cursor(); - if trigger_offset < cursor { - let remove = Transaction::change( - doc.text(), - vec![(trigger_offset, cursor, None)].into_iter(), - ); - doc.apply(&remove, view.id); } + } else { + item.insert_text.as_ref().unwrap_or(&item.label); + unimplemented!(); + // lsp::TextEdit::new(); TODO: calculate a TextEdit from insert_text + // and we insert at position. + }; - use helix_lsp::OffsetEncoding; - let transaction = util::generate_transaction_from_edits( + // if more text was entered, remove it + let cursor = doc.selection(view.id).cursor(); + if trigger_offset < cursor { + let remove = Transaction::change( doc.text(), - vec![edit], - offset_encoding, // TODO: should probably transcode in Client + vec![(trigger_offset, cursor, None)].into_iter(), ); - doc.apply(&transaction, view.id); - - // TODO: merge edit with additional_text_edits - if let Some(additional_edits) = &item.additional_text_edits { - // gopls uses this to add extra imports - if !additional_edits.is_empty() { - let transaction = util::generate_transaction_from_edits( - doc.text(), - additional_edits.clone(), - offset_encoding, // TODO: should probably transcode in Client - ); - doc.apply(&transaction, view.id); - } + doc.apply(&remove, view.id); + } + + use helix_lsp::OffsetEncoding; + let transaction = util::generate_transaction_from_edits( + doc.text(), + vec![edit], + offset_encoding, // TODO: should probably transcode in Client + ); + doc.apply(&transaction, view.id); + + // TODO: merge edit with additional_text_edits + if let Some(additional_edits) = &item.additional_text_edits { + // gopls uses this to add extra imports + if !additional_edits.is_empty() { + let transaction = util::generate_transaction_from_edits( + doc.text(), + additional_edits.clone(), + offset_encoding, // TODO: should probably transcode in Client + ); + doc.apply(&transaction, view.id); } } - _ => (), - }; - }, - ); + } + _ => (), + }; + }); let popup = Popup::new(menu); Self { popup, diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index c771bc65..893c75e7 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -4,8 +4,11 @@ use tui::{ buffer::Buffer as Surface, layout::Rect, style::{Color, Style}, + widgets::Table, }; +pub use tui::widgets::{Cell, Row}; + use std::borrow::Cow; use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; @@ -14,7 +17,15 @@ use fuzzy_matcher::FuzzyMatcher; use helix_core::Position; use helix_view::Editor; -pub struct Menu<T> { +pub trait Item { + // TODO: sort_text + fn filter_text(&self) -> &str; + + fn label(&self) -> &str; + fn row(&self) -> Row; +} + +pub struct Menu<T: Item> { options: Vec<T>, cursor: Option<usize>, @@ -23,19 +34,17 @@ pub struct Menu<T> { /// (index, score) matches: Vec<(usize, i64)>, - format_fn: Box<dyn Fn(&T) -> Cow<str>>, callback_fn: Box<dyn Fn(&mut Editor, Option<&T>, MenuEvent)>, scroll: usize, size: (u16, u16), } -impl<T> Menu<T> { +impl<T: Item> Menu<T> { // TODO: it's like a slimmed down picker, share code? (picker = menu + prompt with different // rendering) pub fn new( options: Vec<T>, - format_fn: impl Fn(&T) -> Cow<str> + 'static, callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static, ) -> Self { let mut menu = Self { @@ -43,7 +52,6 @@ impl<T> Menu<T> { matcher: Box::new(Matcher::default()), matches: Vec::new(), cursor: None, - format_fn: Box::new(format_fn), callback_fn: Box::new(callback_fn), scroll: 0, size: (0, 0), @@ -61,7 +69,6 @@ impl<T> Menu<T> { ref mut options, ref mut matcher, ref mut matches, - ref format_fn, .. } = *self; @@ -72,8 +79,7 @@ impl<T> Menu<T> { .iter() .enumerate() .filter_map(|(index, option)| { - // TODO: maybe using format_fn isn't the best idea here - let text = (format_fn)(option); + let text = option.filter_text(); // TODO: using fuzzy_indices could give us the char idx for match highlighting matcher .fuzzy_match(&text, pattern) @@ -134,7 +140,7 @@ impl<T> Menu<T> { use super::PromptEvent as MenuEvent; -impl<T: 'static> Component for Menu<T> { +impl<T: Item + 'static> Component for Menu<T> { fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult { let event = match event { Event::Key(event) => event, @@ -264,20 +270,26 @@ impl<T: 'static> Component for Menu<T> { let scroll_line = (win_height - scroll_height) * scroll / std::cmp::max(1, len.saturating_sub(win_height)); - for (i, option) in options[scroll..(scroll + win_height).min(len)] - .iter() - .enumerate() - { - let line = Some(i + scroll); - // TODO: set bg for the whole row if selected - surface.set_stringn( - area.x, - area.y + i as u16, - (self.format_fn)(option), - area.width as usize - 1, - if line == self.cursor { selected } else { style }, - ); + use tui::layout::Constraint; + let rows = options.iter().map(|option| option.row()); + let table = Table::new(rows) + .style(style) + .highlight_style(selected) + .column_spacing(1) + .widths(&[Constraint::Percentage(50), Constraint::Percentage(50)]); + + use tui::widgets::TableState; + + table.render_table( + area, + surface, + &mut TableState { + offset: scroll, + selected: self.cursor, + }, + ); + for (i, option) in (scroll..(scroll + win_height).min(len)).enumerate() { let is_marked = i >= scroll_line && i < scroll_line + scroll_height; if is_marked { |