on master help sign in
Display more data in completion popups.
commitb114cfa119bc94396f1ed38109a51183035574ed authorBlaž Hrastnik <blaz@mxxn.io> dateSat 22 May 2021, 17:33 (5 years ago) parentf1dc25a
4 files changed+167−111
helix-term/src/ui/completion.rs+123−80 helix-term/src/ui/menu.rs+34−22 helix-tui/src/widgets/mod.rs+2−2 helix-tui/src/widgets/table.rs+8−7
helix-term/src/ui/completion.rs+123−80
@@ -12,11 +12,63 @@ use helix_core::{Position, Transaction};
1212use helix_view::Editor;
1313
1414use crate::commands;
15−use crate::ui::{Markdown, Menu, Popup, PromptEvent};
15+use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
1616
1717use helix_lsp::lsp;
1818use lsp::CompletionItem;
1919
20+impl menu::Item for CompletionItem {
21+ fn filter_text(&self) -> &str {
22+ self.filter_text
23+ .as_ref()
24+ .unwrap_or_else(|| &self.label)
25+ .as_str()
26+ }
27+
28+ fn label(&self) -> &str {
29+ self.label.as_str()
30+ }
31+
32+ fn row(&self) -> menu::Row {
33+ menu::Row::new(vec![
34+ menu::Cell::from(self.label.as_str()),
35+ menu::Cell::from(match self.kind {
36+ Some(lsp::CompletionItemKind::Text) => "text",
37+ Some(lsp::CompletionItemKind::Method) => "method",
38+ Some(lsp::CompletionItemKind::Function) => "function",
39+ Some(lsp::CompletionItemKind::Constructor) => "constructor",
40+ Some(lsp::CompletionItemKind::Field) => "field",
41+ Some(lsp::CompletionItemKind::Variable) => "variable",
42+ Some(lsp::CompletionItemKind::Class) => "class",
43+ Some(lsp::CompletionItemKind::Interface) => "interface",
44+ Some(lsp::CompletionItemKind::Module) => "module",
45+ Some(lsp::CompletionItemKind::Property) => "property",
46+ Some(lsp::CompletionItemKind::Unit) => "unit",
47+ Some(lsp::CompletionItemKind::Value) => "value",
48+ Some(lsp::CompletionItemKind::Enum) => "enum",
49+ Some(lsp::CompletionItemKind::Keyword) => "keyword",
50+ Some(lsp::CompletionItemKind::Snippet) => "snippet",
51+ Some(lsp::CompletionItemKind::Color) => "color",
52+ Some(lsp::CompletionItemKind::File) => "file",
53+ Some(lsp::CompletionItemKind::Reference) => "reference",
54+ Some(lsp::CompletionItemKind::Folder) => "folder",
55+ Some(lsp::CompletionItemKind::EnumMember) => "enum_member",
56+ Some(lsp::CompletionItemKind::Constant) => "constant",
57+ Some(lsp::CompletionItemKind::Struct) => "struct",
58+ Some(lsp::CompletionItemKind::Event) => "event",
59+ Some(lsp::CompletionItemKind::Operator) => "operator",
60+ Some(lsp::CompletionItemKind::TypeParameter) => "type_param",
61+ None => "",
62+ }),
63+ // self.detail.as_deref().unwrap_or("")
64+ // self.label_details
65+ // .as_ref()
66+ // .or(self.detail())
67+ // .as_str(),
68+ ])
69+ }
70+}
71+
2072/// Wraps a Menu.
2173pub struct Completion {
2274 popup: Popup<Menu<CompletionItem>>, // TODO: Popup<Menu> need to be able to access contents.
@@ -31,92 +83,83 @@ impl Completion {
3183 trigger_offset: usize,
3284 ) -> Self {
3385 // let items: Vec<CompletionItem> = Vec::new();
34− let mut menu = Menu::new(
35− items,
36− |item| {
37− // format_fn
38− item.label.as_str().into()
39−
40− // TODO: use item.filter_text for filtering
41− },
42− move |editor: &mut Editor, item, event| {
43− match event {
44− PromptEvent::Abort => {
45− // revert state
46− // let id = editor.view().doc;
47− // let doc = &mut editor.documents[id];
48− // doc.state = snapshot.clone();
49− }
50− PromptEvent::Validate => {
51− let (view, doc) = editor.current();
52−
53− // revert state to what it was before the last update
54− // doc.state = snapshot.clone();
55−
56− // extract as fn(doc, item):
57−
58− // TODO: need to apply without composing state...
59− // TODO: need to update lsp on accept/cancel by diffing the snapshot with
60− // the final state?
61− // -> on update simply update the snapshot, then on accept redo the call,
62− // finally updating doc.changes + notifying lsp.
63− //
64− // or we could simply use doc.undo + apply when changing between options
65−
66− // always present here
67− let item = item.unwrap();
68−
69− use helix_lsp::{lsp, util};
70− // determine what to insert: text_edit | insert_text | label
71− let edit = if let Some(edit) = &item.text_edit {
72− match edit {
73− lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
74− lsp::CompletionTextEdit::InsertAndReplace(item) => {
75− unimplemented!("completion: insert_and_replace {:?}", item)
76− }
86+ let mut menu = Menu::new(items, move |editor: &mut Editor, item, event| {
87+ match event {
88+ PromptEvent::Abort => {
89+ // revert state
90+ // let id = editor.view().doc;
91+ // let doc = &mut editor.documents[id];
92+ // doc.state = snapshot.clone();
93+ }
94+ PromptEvent::Validate => {
95+ let (view, doc) = editor.current();
96+
97+ // revert state to what it was before the last update
98+ // doc.state = snapshot.clone();
99+
100+ // extract as fn(doc, item):
101+
102+ // TODO: need to apply without composing state...
103+ // TODO: need to update lsp on accept/cancel by diffing the snapshot with
104+ // the final state?
105+ // -> on update simply update the snapshot, then on accept redo the call,
106+ // finally updating doc.changes + notifying lsp.
107+ //
108+ // or we could simply use doc.undo + apply when changing between options
109+
110+ // always present here
111+ let item = item.unwrap();
112+
113+ use helix_lsp::{lsp, util};
114+ // determine what to insert: text_edit | insert_text | label
115+ let edit = if let Some(edit) = &item.text_edit {
116+ match edit {
117+ lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
118+ lsp::CompletionTextEdit::InsertAndReplace(item) => {
119+ unimplemented!("completion: insert_and_replace {:?}", item)
77120 }
78− } else {
79− item.insert_text.as_ref().unwrap_or(&item.label);
80− unimplemented!();
81− // lsp::TextEdit::new(); TODO: calculate a TextEdit from insert_text
82− // and we insert at position.
83− };
84−
85− // if more text was entered, remove it
86− let cursor = doc.selection(view.id).cursor();
87− if trigger_offset < cursor {
88− let remove = Transaction::change(
89− doc.text(),
90− vec![(trigger_offset, cursor, None)].into_iter(),
91− );
92− doc.apply(&remove, view.id);
93121 }
122+ } else {
123+ item.insert_text.as_ref().unwrap_or(&item.label);
124+ unimplemented!();
125+ // lsp::TextEdit::new(); TODO: calculate a TextEdit from insert_text
126+ // and we insert at position.
127+ };
94128
95− use helix_lsp::OffsetEncoding;
96− let transaction = util::generate_transaction_from_edits(
129+ // if more text was entered, remove it
130+ let cursor = doc.selection(view.id).cursor();
131+ if trigger_offset < cursor {
132+ let remove = Transaction::change(
97133 doc.text(),
98− vec![edit],
99− offset_encoding, // TODO: should probably transcode in Client
134+ vec![(trigger_offset, cursor, None)].into_iter(),
100135 );
101− doc.apply(&transaction, view.id);
102−
103− // TODO: merge edit with additional_text_edits
104− if let Some(additional_edits) = &item.additional_text_edits {
105− // gopls uses this to add extra imports
106− if !additional_edits.is_empty() {
107− let transaction = util::generate_transaction_from_edits(
108− doc.text(),
109− additional_edits.clone(),
110− offset_encoding, // TODO: should probably transcode in Client
111− );
112− doc.apply(&transaction, view.id);
113− }
136+ doc.apply(&remove, view.id);
137+ }
138+
139+ use helix_lsp::OffsetEncoding;
140+ let transaction = util::generate_transaction_from_edits(
141+ doc.text(),
142+ vec![edit],
143+ offset_encoding, // TODO: should probably transcode in Client
144+ );
145+ doc.apply(&transaction, view.id);
146+
147+ // TODO: merge edit with additional_text_edits
148+ if let Some(additional_edits) = &item.additional_text_edits {
149+ // gopls uses this to add extra imports
150+ if !additional_edits.is_empty() {
151+ let transaction = util::generate_transaction_from_edits(
152+ doc.text(),
153+ additional_edits.clone(),
154+ offset_encoding, // TODO: should probably transcode in Client
155+ );
156+ doc.apply(&transaction, view.id);
114157 }
115158 }
116− _ => (),
117− };
118− },
119− );
159+ }
160+ _ => (),
161+ };
162+ });
120163 let popup = Popup::new(menu);
121164 Self {
122165 popup,
helix-term/src/ui/menu.rs+34−22
@@ -4,8 +4,11 @@ use tui::{
44 buffer::Buffer as Surface,
55 layout::Rect,
66 style::{Color, Style},
7+ widgets::Table,
78};
89
10+pub use tui::widgets::{Cell, Row};
11+
912use std::borrow::Cow;
1013
1114use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
@@ -14,7 +17,15 @@ use fuzzy_matcher::FuzzyMatcher;
1417use helix_core::Position;
1518use helix_view::Editor;
1619
17−pub struct Menu<T> {
20+pub trait Item {
21+ // TODO: sort_text
22+ fn filter_text(&self) -> &str;
23+
24+ fn label(&self) -> &str;
25+ fn row(&self) -> Row;
26+}
27+
28+pub struct Menu<T: Item> {
1829 options: Vec<T>,
1930
2031 cursor: Option<usize>,
@@ -23,19 +34,17 @@ pub struct Menu<T> {
2334 /// (index, score)
2435 matches: Vec<(usize, i64)>,
2536
26− format_fn: Box<dyn Fn(&T) -> Cow<str>>,
2737 callback_fn: Box<dyn Fn(&mut Editor, Option<&T>, MenuEvent)>,
2838
2939 scroll: usize,
3040 size: (u16, u16),
3141}
3242
33−impl<T> Menu<T> {
43+impl<T: Item> Menu<T> {
3444 // TODO: it's like a slimmed down picker, share code? (picker = menu + prompt with different
3545 // rendering)
3646 pub fn new(
3747 options: Vec<T>,
38− format_fn: impl Fn(&T) -> Cow<str> + 'static,
3948 callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static,
4049 ) -> Self {
4150 let mut menu = Self {
@@ -43,7 +52,6 @@ impl<T> Menu<T> {
4352 matcher: Box::new(Matcher::default()),
4453 matches: Vec::new(),
4554 cursor: None,
46− format_fn: Box::new(format_fn),
4755 callback_fn: Box::new(callback_fn),
4856 scroll: 0,
4957 size: (0, 0),
@@ -61,7 +69,6 @@ impl<T> Menu<T> {
6169 ref mut options,
6270 ref mut matcher,
6371 ref mut matches,
64− ref format_fn,
6572 ..
6673 } = *self;
6774
@@ -72,8 +79,7 @@ impl<T> Menu<T> {
7279 .iter()
7380 .enumerate()
7481 .filter_map(|(index, option)| {
75− // TODO: maybe using format_fn isn't the best idea here
76− let text = (format_fn)(option);
82+ let text = option.filter_text();
7783 // TODO: using fuzzy_indices could give us the char idx for match highlighting
7884 matcher
7985 .fuzzy_match(&text, pattern)
@@ -134,7 +140,7 @@ impl<T> Menu<T> {
134140
135141use super::PromptEvent as MenuEvent;
136142
137−impl<T: 'static> Component for Menu<T> {
143+impl<T: Item + 'static> Component for Menu<T> {
138144 fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
139145 let event = match event {
140146 Event::Key(event) => event,
@@ -264,20 +270,26 @@ impl<T: 'static> Component for Menu<T> {
264270 let scroll_line = (win_height - scroll_height) * scroll
265271 / std::cmp::max(1, len.saturating_sub(win_height));
266272
267− for (i, option) in options[scroll..(scroll + win_height).min(len)]
268− .iter()
269− .enumerate()
270− {
271− let line = Some(i + scroll);
272− // TODO: set bg for the whole row if selected
273− surface.set_stringn(
274− area.x,
275− area.y + i as u16,
276− (self.format_fn)(option),
277− area.width as usize - 1,
278− if line == self.cursor { selected } else { style },
279− );
273+ use tui::layout::Constraint;
274+ let rows = options.iter().map(|option| option.row());
275+ let table = Table::new(rows)
276+ .style(style)
277+ .highlight_style(selected)
278+ .column_spacing(1)
279+ .widths(&[Constraint::Percentage(50), Constraint::Percentage(50)]);
280+
281+ use tui::widgets::TableState;
282+
283+ table.render_table(
284+ area,
285+ surface,
286+ &mut TableState {
287+ offset: scroll,
288+ selected: self.cursor,
289+ },
290+ );
280291
292+ for (i, option) in (scroll..(scroll + win_height).min(len)).enumerate() {
281293 let is_marked = i >= scroll_line && i < scroll_line + scroll_height;
282294
283295 if is_marked {
helix-tui/src/widgets/mod.rs+2−2
@@ -13,12 +13,12 @@ mod block;
1313// mod list;
1414mod paragraph;
1515mod reflow;
16−// mod table;
16+mod table;
1717
1818pub use self::block::{Block, BorderType};
1919// pub use self::list::{List, ListItem, ListState};
2020pub use self::paragraph::{Paragraph, Wrap};
21−// pub use self::table::{Cell, Row, Table, TableState};
21+pub use self::table::{Cell, Row, Table, TableState};
2222
2323use crate::{buffer::Buffer, layout::Rect};
2424use bitflags::bitflags;
helix-tui/src/widgets/table.rs+8−7
@@ -3,7 +3,7 @@ use crate::{
33 layout::{Constraint, Rect},
44 style::Style,
55 text::Text,
6− widgets::{Block, StatefulWidget, Widget},
6+ widgets::{Block, Widget},
77};
88use cassowary::{
99 strength::{MEDIUM, REQUIRED, WEAK},
@@ -368,8 +368,8 @@ impl<'a> Table<'a> {
368368
369369#[derive(Debug, Clone)]
370370pub struct TableState {
371− offset: usize,
372− selected: Option<usize>,
371+ pub offset: usize,
372+ pub selected: Option<usize>,
373373}
374374
375375impl Default for TableState {
@@ -394,10 +394,11 @@ impl TableState {
394394 }
395395}
396396
397−impl<'a> StatefulWidget for Table<'a> {
398− type State = TableState;
397+// impl<'a> StatefulWidget for Table<'a> {
398+impl<'a> Table<'a> {
399+ // type State = TableState;
399400
400− fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
401+ pub fn render_table(mut self, area: Rect, buf: &mut Buffer, state: &mut TableState) {
401402 if area.area() == 0 {
402403 return;
403404 }
@@ -522,7 +523,7 @@ fn render_cell(buf: &mut Buffer, cell: &Cell, area: Rect) {
522523impl<'a> Widget for Table<'a> {
523524 fn render(self, area: Rect, buf: &mut Buffer) {
524525 let mut state = TableState::default();
525− StatefulWidget::render(self, area, buf, &mut state);
526+ Table::render_table(self, area, buf, &mut state);
526527 }
527528}
528529