From b114cfa119bc94396f1ed38109a51183035574ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Sat, 22 May 2021 17:33:42 +0900 Subject: [PATCH] Display more data in completion popups. --- helix-term/src/ui/completion.rs | 203 +++++++++++++++++++------------- helix-term/src/ui/menu.rs | 56 +++++---- helix-tui/src/widgets/mod.rs | 4 +- helix-tui/src/widgets/table.rs | 15 +-- 4 files changed, 167 insertions(+), 111 deletions(-) diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 719daa0f5..f79c22352 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>, // TODO: Popup need to be able to access contents. @@ -31,92 +83,83 @@ impl Completion { trigger_offset: usize, ) -> Self { // let items: Vec = 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 c771bc652..893c75e73 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 { +pub trait Item { + // TODO: sort_text + fn filter_text(&self) -> &str; + + fn label(&self) -> &str; + fn row(&self) -> Row; +} + +pub struct Menu { options: Vec, cursor: Option, @@ -23,19 +34,17 @@ pub struct Menu { /// (index, score) matches: Vec<(usize, i64)>, - format_fn: Box Cow>, callback_fn: Box, MenuEvent)>, scroll: usize, size: (u16, u16), } -impl Menu { +impl Menu { // TODO: it's like a slimmed down picker, share code? (picker = menu + prompt with different // rendering) pub fn new( options: Vec, - format_fn: impl Fn(&T) -> Cow + 'static, callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static, ) -> Self { let mut menu = Self { @@ -43,7 +52,6 @@ impl Menu { 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 Menu { ref mut options, ref mut matcher, ref mut matches, - ref format_fn, .. } = *self; @@ -72,8 +79,7 @@ impl Menu { .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 Menu { use super::PromptEvent as MenuEvent; -impl Component for Menu { +impl Component for Menu { fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult { let event = match event { Event::Key(event) => event, @@ -264,20 +270,26 @@ impl Component for Menu { 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 { diff --git a/helix-tui/src/widgets/mod.rs b/helix-tui/src/widgets/mod.rs index 00744a1ca..e334b8941 100644 --- a/helix-tui/src/widgets/mod.rs +++ b/helix-tui/src/widgets/mod.rs @@ -13,12 +13,12 @@ mod block; // mod list; mod paragraph; mod reflow; -// mod table; +mod table; pub use self::block::{Block, BorderType}; // pub use self::list::{List, ListItem, ListState}; pub use self::paragraph::{Paragraph, Wrap}; -// pub use self::table::{Cell, Row, Table, TableState}; +pub use self::table::{Cell, Row, Table, TableState}; use crate::{buffer::Buffer, layout::Rect}; use bitflags::bitflags; diff --git a/helix-tui/src/widgets/table.rs b/helix-tui/src/widgets/table.rs index 31624a8fb..d42d7d304 100644 --- a/helix-tui/src/widgets/table.rs +++ b/helix-tui/src/widgets/table.rs @@ -3,7 +3,7 @@ use crate::{ layout::{Constraint, Rect}, style::Style, text::Text, - widgets::{Block, StatefulWidget, Widget}, + widgets::{Block, Widget}, }; use cassowary::{ strength::{MEDIUM, REQUIRED, WEAK}, @@ -368,8 +368,8 @@ impl<'a> Table<'a> { #[derive(Debug, Clone)] pub struct TableState { - offset: usize, - selected: Option, + pub offset: usize, + pub selected: Option, } impl Default for TableState { @@ -394,10 +394,11 @@ impl TableState { } } -impl<'a> StatefulWidget for Table<'a> { - type State = TableState; +// impl<'a> StatefulWidget for Table<'a> { +impl<'a> Table<'a> { + // type State = TableState; - fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { + pub fn render_table(mut self, area: Rect, buf: &mut Buffer, state: &mut TableState) { if area.area() == 0 { return; } @@ -522,7 +523,7 @@ fn render_cell(buf: &mut Buffer, cell: &Cell, area: Rect) { impl<'a> Widget for Table<'a> { fn render(self, area: Rect, buf: &mut Buffer) { let mut state = TableState::default(); - StatefulWidget::render(self, area, buf, &mut state); + Table::render_table(self, area, buf, &mut state); } }