on master help sign in
Display more data in completion popups.
b114cfaBlaž Hrastnik
helix-term/src/ui/completion.rs+123−80
@@ -12,11 +12,63 @@ use helix_core::{Position, Transaction};
12use helix_view::Editor;
12use helix_view::Editor;
13
13
14use crate::commands;
14use crate::commands;
15−use crate::ui::{Markdown, Menu, Popup, PromptEvent};
15+use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
16
16
17use helix_lsp::lsp;
17use helix_lsp::lsp;
18use lsp::CompletionItem;
18use lsp::CompletionItem;
19
19
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+
20/// Wraps a Menu.
72/// Wraps a Menu.
21pub struct Completion {
73pub struct Completion {
22 popup: Popup<Menu<CompletionItem>>, // TODO: Popup<Menu> need to be able to access contents.
74 popup: Popup<Menu<CompletionItem>>, // TODO: Popup<Menu> need to be able to access contents.
@@ -31,92 +83,83 @@ impl Completion {
31 trigger_offset: usize,
83 trigger_offset: usize,
32 ) -> Self {
84 ) -> Self {
33 // let items: Vec<CompletionItem> = Vec::new();
85 // 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| {
86+ let mut menu = Menu::new(items, move |editor: &mut Editor, item, event| {
43− match event {
87+ match event {
44− PromptEvent::Abort => {
88+ PromptEvent::Abort => {
45− // revert state
89+ // revert state
46− // let id = editor.view().doc;
90+ // let id = editor.view().doc;
47− // let doc = &mut editor.documents[id];
91+ // let doc = &mut editor.documents[id];
48− // doc.state = snapshot.clone();
92+ // doc.state = snapshot.clone();
49− }
93+ }
50− PromptEvent::Validate => {
94+ PromptEvent::Validate => {
51− let (view, doc) = editor.current();
95+ let (view, doc) = editor.current();
52−
96+
53− // revert state to what it was before the last update
97+ // revert state to what it was before the last update
54− // doc.state = snapshot.clone();
98+ // doc.state = snapshot.clone();
55−
99+
56− // extract as fn(doc, item):
100+ // extract as fn(doc, item):
57−
101+
58− // TODO: need to apply without composing state...
102+ // TODO: need to apply without composing state...
59− // TODO: need to update lsp on accept/cancel by diffing the snapshot with
103+ // TODO: need to update lsp on accept/cancel by diffing the snapshot with
60− // the final state?
104+ // the final state?
61− // -> on update simply update the snapshot, then on accept redo the call,
105+ // -> on update simply update the snapshot, then on accept redo the call,
62− // finally updating doc.changes + notifying lsp.
106+ // finally updating doc.changes + notifying lsp.
63− //
107+ //
64− // or we could simply use doc.undo + apply when changing between options
108+ // or we could simply use doc.undo + apply when changing between options
65−
109+
66− // always present here
110+ // always present here
67− let item = item.unwrap();
111+ let item = item.unwrap();
68−
112+
69− use helix_lsp::{lsp, util};
113+ use helix_lsp::{lsp, util};
70− // determine what to insert: text_edit | insert_text | label
114+ // determine what to insert: text_edit | insert_text | label
71− let edit = if let Some(edit) = &item.text_edit {
115+ let edit = if let Some(edit) = &item.text_edit {
72− match edit {
116+ match edit {
73− lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
117+ lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
74− lsp::CompletionTextEdit::InsertAndReplace(item) => {
118+ lsp::CompletionTextEdit::InsertAndReplace(item) => {
75− unimplemented!("completion: insert_and_replace {:?}", item)
119+ unimplemented!("completion: insert_and_replace {:?}", item)
76− }
77 }
120 }
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);
93 }
121 }
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+ };
94
128
95− use helix_lsp::OffsetEncoding;
129+ // if more text was entered, remove it
96− let transaction = util::generate_transaction_from_edits(
130+ let cursor = doc.selection(view.id).cursor();
131+ if trigger_offset < cursor {
132+ let remove = Transaction::change(
97 doc.text(),
133 doc.text(),
98− vec![edit],
134+ vec![(trigger_offset, cursor, None)].into_iter(),
99− offset_encoding, // TODO: should probably transcode in Client
100 );
135 );
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+ );
101− doc.apply(&transaction, view.id);
145+ doc.apply(&transaction, view.id);
102−
146+
103− // TODO: merge edit with additional_text_edits
147+ // TODO: merge edit with additional_text_edits
104− if let Some(additional_edits) = &item.additional_text_edits {
148+ if let Some(additional_edits) = &item.additional_text_edits {
105− // gopls uses this to add extra imports
149+ // gopls uses this to add extra imports
106− if !additional_edits.is_empty() {
150+ if !additional_edits.is_empty() {
107− let transaction = util::generate_transaction_from_edits(
151+ let transaction = util::generate_transaction_from_edits(
108− doc.text(),
152+ doc.text(),
109− additional_edits.clone(),
153+ additional_edits.clone(),
110− offset_encoding, // TODO: should probably transcode in Client
154+ offset_encoding, // TODO: should probably transcode in Client
111− );
155+ );
112− doc.apply(&transaction, view.id);
156+ doc.apply(&transaction, view.id);
113− }
114 }
157 }
115 }
158 }
159+ }
116− _ => (),
160+ _ => (),
117− };
118− },
161+ };
119− );
162+ });
120 let popup = Popup::new(menu);
163 let popup = Popup::new(menu);
121 Self {
164 Self {
122 popup,
165 popup,
helix-term/src/ui/menu.rs+34−22
@@ -4,8 +4,11 @@ use tui::{
4 buffer::Buffer as Surface,
4 buffer::Buffer as Surface,
5 layout::Rect,
5 layout::Rect,
6 style::{Color, Style},
6 style::{Color, Style},
7+ widgets::Table,
7};
8};
8
9
10+pub use tui::widgets::{Cell, Row};
11+
9use std::borrow::Cow;
12use std::borrow::Cow;
10
13
11use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
14use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
@@ -14,7 +17,15 @@ use fuzzy_matcher::FuzzyMatcher;
14use helix_core::Position;
17use helix_core::Position;
15use helix_view::Editor;
18use helix_view::Editor;
16
19
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+
17−pub struct Menu<T> {
28+pub struct Menu<T: Item> {
18 options: Vec<T>,
29 options: Vec<T>,
19
30
20 cursor: Option<usize>,
31 cursor: Option<usize>,
@@ -23,19 +34,17 @@ pub struct Menu<T> {
23 /// (index, score)
34 /// (index, score)
24 matches: Vec<(usize, i64)>,
35 matches: Vec<(usize, i64)>,
25
36
26− format_fn: Box<dyn Fn(&T) -> Cow<str>>,
27 callback_fn: Box<dyn Fn(&mut Editor, Option<&T>, MenuEvent)>,
37 callback_fn: Box<dyn Fn(&mut Editor, Option<&T>, MenuEvent)>,
28
38
29 scroll: usize,
39 scroll: usize,
30 size: (u16, u16),
40 size: (u16, u16),
31}
41}
32
42
33−impl<T> Menu<T> {
43+impl<T: Item> Menu<T> {
34 // TODO: it's like a slimmed down picker, share code? (picker = menu + prompt with different
44 // TODO: it's like a slimmed down picker, share code? (picker = menu + prompt with different
35 // rendering)
45 // rendering)
36 pub fn new(
46 pub fn new(
37 options: Vec<T>,
47 options: Vec<T>,
38− format_fn: impl Fn(&T) -> Cow<str> + 'static,
39 callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static,
48 callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static,
40 ) -> Self {
49 ) -> Self {
41 let mut menu = Self {
50 let mut menu = Self {
@@ -43,7 +52,6 @@ impl<T> Menu<T> {
43 matcher: Box::new(Matcher::default()),
52 matcher: Box::new(Matcher::default()),
44 matches: Vec::new(),
53 matches: Vec::new(),
45 cursor: None,
54 cursor: None,
46− format_fn: Box::new(format_fn),
47 callback_fn: Box::new(callback_fn),
55 callback_fn: Box::new(callback_fn),
48 scroll: 0,
56 scroll: 0,
49 size: (0, 0),
57 size: (0, 0),
@@ -61,7 +69,6 @@ impl<T> Menu<T> {
61 ref mut options,
69 ref mut options,
62 ref mut matcher,
70 ref mut matcher,
63 ref mut matches,
71 ref mut matches,
64− ref format_fn,
65 ..
72 ..
66 } = *self;
73 } = *self;
67
74
@@ -72,8 +79,7 @@ impl<T> Menu<T> {
72 .iter()
79 .iter()
73 .enumerate()
80 .enumerate()
74 .filter_map(|(index, option)| {
81 .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();
77 // TODO: using fuzzy_indices could give us the char idx for match highlighting
83 // TODO: using fuzzy_indices could give us the char idx for match highlighting
78 matcher
84 matcher
79 .fuzzy_match(&text, pattern)
85 .fuzzy_match(&text, pattern)
@@ -134,7 +140,7 @@ impl<T> Menu<T> {
134
140
135use super::PromptEvent as MenuEvent;
141use super::PromptEvent as MenuEvent;
136
142
137−impl<T: 'static> Component for Menu<T> {
143+impl<T: Item + 'static> Component for Menu<T> {
138 fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
144 fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
139 let event = match event {
145 let event = match event {
140 Event::Key(event) => event,
146 Event::Key(event) => event,
@@ -264,20 +270,26 @@ impl<T: 'static> Component for Menu<T> {
264 let scroll_line = (win_height - scroll_height) * scroll
270 let scroll_line = (win_height - scroll_height) * scroll
265 / std::cmp::max(1, len.saturating_sub(win_height));
271 / std::cmp::max(1, len.saturating_sub(win_height));
266
272
267− for (i, option) in options[scroll..(scroll + win_height).min(len)]
273+ use tui::layout::Constraint;
268− .iter()
274+ let rows = options.iter().map(|option| option.row());
269− .enumerate()
275+ let table = Table::new(rows)
270− {
276+ .style(style)
271− let line = Some(i + scroll);
277+ .highlight_style(selected)
272− // TODO: set bg for the whole row if 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,
273− surface.set_stringn(
285+ surface,
274− area.x,
286+ &mut TableState {
275− area.y + i as u16,
287+ offset: scroll,
276− (self.format_fn)(option),
288+ selected: self.cursor,
277− area.width as usize - 1,
289+ },
278− if line == self.cursor { selected } else { style },
279− );
290+ );
280
291
292+ for (i, option) in (scroll..(scroll + win_height).min(len)).enumerate() {
281 let is_marked = i >= scroll_line && i < scroll_line + scroll_height;
293 let is_marked = i >= scroll_line && i < scroll_line + scroll_height;
282
294
283 if is_marked {
295 if is_marked {
helix-tui/src/widgets/mod.rs+2−2
@@ -13,12 +13,12 @@ mod block;
13// mod list;
13// mod list;
14mod paragraph;
14mod paragraph;
15mod reflow;
15mod reflow;
16−// mod table;
16+mod table;
17
17
18pub use self::block::{Block, BorderType};
18pub use self::block::{Block, BorderType};
19// pub use self::list::{List, ListItem, ListState};
19// pub use self::list::{List, ListItem, ListState};
20pub use self::paragraph::{Paragraph, Wrap};
20pub use self::paragraph::{Paragraph, Wrap};
21−// pub use self::table::{Cell, Row, Table, TableState};
21+pub use self::table::{Cell, Row, Table, TableState};
22
22
23use crate::{buffer::Buffer, layout::Rect};
23use crate::{buffer::Buffer, layout::Rect};
24use bitflags::bitflags;
24use bitflags::bitflags;
helix-tui/src/widgets/table.rs+8−7
@@ -3,7 +3,7 @@ use crate::{
3 layout::{Constraint, Rect},
3 layout::{Constraint, Rect},
4 style::Style,
4 style::Style,
5 text::Text,
5 text::Text,
6− widgets::{Block, StatefulWidget, Widget},
6+ widgets::{Block, Widget},
7};
7};
8use cassowary::{
8use cassowary::{
9 strength::{MEDIUM, REQUIRED, WEAK},
9 strength::{MEDIUM, REQUIRED, WEAK},
@@ -368,8 +368,8 @@ impl<'a> Table<'a> {
368
368
369#[derive(Debug, Clone)]
369#[derive(Debug, Clone)]
370pub struct TableState {
370pub struct TableState {
371− offset: usize,
371+ pub offset: usize,
372− selected: Option<usize>,
372+ pub selected: Option<usize>,
373}
373}
374
374
375impl Default for TableState {
375impl Default for TableState {
@@ -394,10 +394,11 @@ impl TableState {
394 }
394 }
395}
395}
396
396
397−impl<'a> StatefulWidget for Table<'a> {
397+// impl<'a> StatefulWidget for Table<'a> {
398+impl<'a> Table<'a> {
398− type State = TableState;
399+ // type State = TableState;
399
400
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) {
401 if area.area() == 0 {
402 if area.area() == 0 {
402 return;
403 return;
403 }
404 }
@@ -522,7 +523,7 @@ fn render_cell(buf: &mut Buffer, cell: &Cell, area: Rect) {
522impl<'a> Widget for Table<'a> {
523impl<'a> Widget for Table<'a> {
523 fn render(self, area: Rect, buf: &mut Buffer) {
524 fn render(self, area: Rect, buf: &mut Buffer) {
524 let mut state = TableState::default();
525 let mut state = TableState::default();
525− StatefulWidget::render(self, area, buf, &mut state);
526+ Table::render_table(self, area, buf, &mut state);
526 }
527 }
527}
528}
528
529