on master help sign in
Filter the completion menu based on text entered.
commita24c3fff54f319eac42ae6947b910b54ee6fd899 authorBlaž Hrastnik <blaz@mxxn.io> dateSat 27 Mar 2021, 12:06 (6 years ago) parent2a3910c
6 files changed+261−109
helix-term/src/commands.rs+32−98 helix-term/src/compositor.rs+4−1 helix-term/src/ui/completion.rs+149 helix-term/src/ui/menu.rs+65−5 helix-term/src/ui/mod.rs+2 helix-term/src/ui/popup.rs+9−5
helix-term/src/commands.rs+32−98
@@ -11,7 +11,7 @@ use once_cell::sync::Lazy;
1111
1212use crate::{
1313 compositor::{Callback, Component, Compositor},
14− ui::{self, Picker, Popup, Prompt, PromptEvent},
14+ ui::{self, Completion, Picker, Popup, Prompt, PromptEvent},
1515};
1616
1717use std::path::PathBuf;
@@ -55,12 +55,7 @@ impl<'a> Context<'a> {
5555 /// Push a new component onto the compositor.
5656 pub fn push_layer(&mut self, mut component: Box<dyn Component>) {
5757 self.callback = Some(Box::new(
58− |compositor: &mut Compositor, editor: &mut Editor| {
59− let size = compositor.size();
60− // trigger required_size on init
61− component.required_size((size.width, size.height));
62− compositor.push(component);
63− },
58+ |compositor: &mut Compositor, editor: &mut Editor| compositor.push(component),
6459 ));
6560 }
6661
@@ -1604,10 +1599,28 @@ pub fn completion(cx: &mut Context) {
16041599 // // downcast dyn Component to Completion component
16051600 // // emit response to completion (completion.complete/handle(response))
16061601 // })
1607− // async {
1608− // let (response, callback) = response.await?;
1609− // callback(response)
1610− // }
1602+ //
1603+ // typing after prompt opens: usually start offset is tracked and everything between
1604+ // start_offset..cursor is replaced. For our purposes we could keep the start state (doc,
1605+ // selection) and revert to them before applying. This needs to properly reset changes/history
1606+ // though...
1607+ //
1608+ // company-mode does this by matching the prefix of the completion and removing it.
1609+
1610+ // ignore isIncomplete for now
1611+ // keep state while typing
1612+ // the behavior should be, filter the menu based on input
1613+ // if items returns empty at any point, remove the popup
1614+ // if backspace past initial offset point, remove the popup
1615+ //
1616+ // debounce requests!
1617+ //
1618+ // need an idle timeout thing.
1619+ // https://github.com/company-mode/company-mode/blob/master/company.el#L620-L622
1620+ //
1621+ // "The idle delay in seconds until completion starts automatically.
1622+ // The prefix still has to satisfy `company-minimum-prefix-length' before that
1623+ // happens. The value of nil means no idle completion."
16111624
16121625 let doc = cx.doc();
16131626
@@ -1623,11 +1636,13 @@ pub fn completion(cx: &mut Context) {
16231636
16241637 let res = smol::block_on(language_server.completion(doc.identifier(), pos)).unwrap();
16251638
1639+ let trigger_offset = doc.selection().cursor();
1640+
16261641 cx.callback(
16271642 res,
1628− |editor: &mut Editor,
1629− compositor: &mut Compositor,
1630− response: Option<lsp::CompletionResponse>| {
1643+ move |editor: &mut Editor,
1644+ compositor: &mut Compositor,
1645+ response: Option<lsp::CompletionResponse>| {
16311646 let items = match response {
16321647 Some(lsp::CompletionResponse::Array(items)) => items,
16331648 // TODO: do something with is_incomplete
@@ -1640,92 +1655,11 @@ pub fn completion(cx: &mut Context) {
16401655
16411656 // TODO: if no completion, show some message or something
16421657 if !items.is_empty() {
1643− // let snapshot = doc.state.clone();
1644− let mut menu = ui::Menu::new(
1645− items,
1646− |item| {
1647− // format_fn
1648− item.label.as_str().into()
1649−
1650− // TODO: use item.filter_text for filtering
1651− },
1652− move |editor: &mut Editor, item, event| {
1653− match event {
1654− PromptEvent::Abort => {
1655− // revert state
1656− // let id = editor.view().doc;
1657− // let doc = &mut editor.documents[id];
1658− // doc.state = snapshot.clone();
1659− }
1660− PromptEvent::Validate => {
1661− let id = editor.view().doc;
1662− let doc = &mut editor.documents[id];
1663−
1664− // revert state to what it was before the last update
1665− // doc.state = snapshot.clone();
1666−
1667− // extract as fn(doc, item):
1668−
1669− // TODO: need to apply without composing state...
1670− // TODO: need to update lsp on accept/cancel by diffing the snapshot with
1671− // the final state?
1672− // -> on update simply update the snapshot, then on accept redo the call,
1673− // finally updating doc.changes + notifying lsp.
1674− //
1675− // or we could simply use doc.undo + apply when changing between options
1676−
1677− // always present here
1678− let item = item.unwrap();
1679−
1680− use helix_lsp::{lsp, util};
1681− // determine what to insert: text_edit | insert_text | label
1682− let edit = if let Some(edit) = &item.text_edit {
1683− match edit {
1684− lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
1685− lsp::CompletionTextEdit::InsertAndReplace(item) => {
1686− unimplemented!(
1687− "completion: insert_and_replace {:?}",
1688− item
1689− )
1690− }
1691− }
1692− } else {
1693− item.insert_text.as_ref().unwrap_or(&item.label);
1694− unimplemented!();
1695− // lsp::TextEdit::new(); TODO: calculate a TextEdit from insert_text
1696− // and we insert at position.
1697− };
1698−
1699− // TODO: merge edit with additional_text_edits
1700− if let Some(additional_edits) = &item.additional_text_edits {
1701− if !additional_edits.is_empty() {
1702− unimplemented!(
1703− "completion: additional_text_edits: {:?}",
1704− additional_edits
1705− );
1706− }
1707− }
1708−
1709− let transaction =
1710− util::generate_transaction_from_edits(doc.text(), vec![edit]);
1711− doc.apply(&transaction);
1712− // TODO: doc.append_changes_to_history(); if not in insert mode?
1713− }
1714− _ => (),
1715− };
1716− },
1717− );
1718−
1719− let popup = Popup::new(Box::new(menu));
1720− let mut component: Box<dyn Component> = Box::new(popup);
1658+ let completion = Completion::new(items, trigger_offset);
17211659
17221660 // Server error: content modified
17231661
1724− // TODO: this is shared with cx.push_layer
1725− let size = compositor.size();
1726− // trigger required_size on init
1727− component.required_size((size.width, size.height));
1728− compositor.push(component);
1662+ compositor.push(Box::new(completion));
17291663 }
17301664 },
17311665 );
@@ -1774,7 +1708,7 @@ pub fn hover(cx: &mut Context) {
17741708 // skip if contents empty
17751709
17761710 let contents = ui::Markdown::new(contents);
1777− let mut popup = Popup::new(Box::new(contents));
1711+ let mut popup = Popup::new(contents);
17781712 cx.push_layer(Box::new(popup));
17791713 }
17801714}
helix-term/src/compositor.rs+4−1
@@ -92,7 +92,10 @@ impl Compositor {
9292 .expect("Unable to resize terminal")
9393 }
9494
95− pub fn push(&mut self, layer: Box<dyn Component>) {
95+ pub fn push(&mut self, mut layer: Box<dyn Component>) {
96+ let size = self.size();
97+ // trigger required_size on init
98+ layer.required_size((size.width, size.height));
9699 self.layers.push(layer);
97100 }
98101
helix-term/src/ui/completion.rsnew file+149
@@ -0,0 +1,149 @@
1+use crate::compositor::{Component, Compositor, Context, EventResult};
2+use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
3+use tui::{
4+ buffer::Buffer as Surface,
5+ layout::Rect,
6+ style::{Color, Style},
7+ widgets::{Block, Borders},
8+};
9+
10+use std::borrow::Cow;
11+
12+use helix_core::{Position, Transaction};
13+use helix_view::Editor;
14+
15+use crate::ui::{Menu, Popup, PromptEvent};
16+
17+use helix_lsp::lsp;
18+use lsp::CompletionItem;
19+
20+/// Wraps a Menu.
21+pub struct Completion {
22+ popup: Popup<Menu<CompletionItem>>, // TODO: Popup<Menu> need to be able to access contents.
23+ trigger_offset: usize,
24+}
25+
26+impl Completion {
27+ pub fn new(items: Vec<CompletionItem>, trigger_offset: usize) -> Self {
28+ // let items: Vec<CompletionItem> = Vec::new();
29+ let mut menu = Menu::new(
30+ items,
31+ |item| {
32+ // format_fn
33+ item.label.as_str().into()
34+
35+ // TODO: use item.filter_text for filtering
36+ },
37+ move |editor: &mut Editor, item, event| {
38+ match event {
39+ PromptEvent::Abort => {
40+ // revert state
41+ // let id = editor.view().doc;
42+ // let doc = &mut editor.documents[id];
43+ // doc.state = snapshot.clone();
44+ }
45+ PromptEvent::Validate => {
46+ let id = editor.view().doc;
47+ let doc = &mut editor.documents[id];
48+
49+ // revert state to what it was before the last update
50+ // doc.state = snapshot.clone();
51+
52+ // extract as fn(doc, item):
53+
54+ // TODO: need to apply without composing state...
55+ // TODO: need to update lsp on accept/cancel by diffing the snapshot with
56+ // the final state?
57+ // -> on update simply update the snapshot, then on accept redo the call,
58+ // finally updating doc.changes + notifying lsp.
59+ //
60+ // or we could simply use doc.undo + apply when changing between options
61+
62+ // always present here
63+ let item = item.unwrap();
64+
65+ use helix_lsp::{lsp, util};
66+ // determine what to insert: text_edit | insert_text | label
67+ let edit = if let Some(edit) = &item.text_edit {
68+ match edit {
69+ lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
70+ lsp::CompletionTextEdit::InsertAndReplace(item) => {
71+ unimplemented!("completion: insert_and_replace {:?}", item)
72+ }
73+ }
74+ } else {
75+ item.insert_text.as_ref().unwrap_or(&item.label);
76+ unimplemented!();
77+ // lsp::TextEdit::new(); TODO: calculate a TextEdit from insert_text
78+ // and we insert at position.
79+ };
80+
81+ // TODO: merge edit with additional_text_edits
82+ if let Some(additional_edits) = &item.additional_text_edits {
83+ if !additional_edits.is_empty() {
84+ unimplemented!(
85+ "completion: additional_text_edits: {:?}",
86+ additional_edits
87+ );
88+ }
89+ }
90+
91+ // if more text was entered, remove it
92+ let cursor = doc.selection().cursor();
93+ if trigger_offset < cursor {
94+ let remove = Transaction::change(
95+ doc.text(),
96+ vec![(trigger_offset, cursor, None)].into_iter(),
97+ );
98+ doc.apply(&remove);
99+ }
100+
101+ let transaction =
102+ util::generate_transaction_from_edits(doc.text(), vec![edit]);
103+ doc.apply(&transaction);
104+ }
105+ _ => (),
106+ };
107+ },
108+ );
109+ let popup = Popup::new(menu);
110+ Self {
111+ popup,
112+ trigger_offset,
113+ }
114+ }
115+}
116+
117+impl Component for Completion {
118+ fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
119+ // input
120+ if let Event::Key(KeyEvent {
121+ code: KeyCode::Char(ch),
122+ ..
123+ }) = event
124+ {
125+ // recompute menu based on matches
126+ let menu = self.popup.contents();
127+ let id = cx.editor.view().doc;
128+ let doc = cx.editor.document(id).unwrap();
129+
130+ let cursor = doc.selection().cursor();
131+ if self.trigger_offset <= cursor {
132+ let fragment = doc.text().slice(self.trigger_offset..cursor);
133+ let text = Cow::from(fragment);
134+ // TODO: logic is same as ui/picker
135+ menu.score(&text);
136+ }
137+ }
138+
139+ self.popup.handle_event(event, cx)
140+ }
141+
142+ fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
143+ self.popup.required_size(viewport)
144+ }
145+
146+ fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
147+ self.popup.render(area, surface, cx)
148+ }
149+}
helix-term/src/ui/menu.rs+65−5
@@ -9,6 +9,9 @@ use tui::{
99
1010use std::borrow::Cow;
1111
12+use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
13+use fuzzy_matcher::FuzzyMatcher;
14+
1215use helix_core::Position;
1316use helix_view::Editor;
1417
@@ -17,6 +20,10 @@ pub struct Menu<T> {
1720
1821 cursor: Option<usize>,
1922
23+ matcher: Box<Matcher>,
24+ /// (index, score)
25+ matches: Vec<(usize, i64)>,
26+
2027 format_fn: Box<dyn Fn(&T) -> Cow<str>>,
2128 callback_fn: Box<dyn Fn(&mut Editor, Option<&T>, MenuEvent)>,
2229
@@ -32,14 +39,53 @@ impl<T> Menu<T> {
3239 format_fn: impl Fn(&T) -> Cow<str> + 'static,
3340 callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static,
3441 ) -> Self {
35− Self {
42+ let mut menu = Self {
3643 options,
44+ matcher: Box::new(Matcher::default()),
45+ matches: Vec::new(),
3746 cursor: None,
3847 format_fn: Box::new(format_fn),
3948 callback_fn: Box::new(callback_fn),
4049 scroll: 0,
4150 size: (0, 0),
42− }
51+ };
52+
53+ // TODO: scoring on empty input should just use a fastpath
54+ menu.score("");
55+
56+ menu
57+ }
58+
59+ pub fn score(&mut self, pattern: &str) {
60+ // need to borrow via pattern match otherwise it complains about simultaneous borrow
61+ let Self {
62+ ref mut options,
63+ ref mut matcher,
64+ ref mut matches,
65+ ref format_fn,
66+ ..
67+ } = *self;
68+
69+ // reuse the matches allocation
70+ matches.clear();
71+ matches.extend(
72+ self.options
73+ .iter()
74+ .enumerate()
75+ .filter_map(|(index, option)| {
76+ // TODO: maybe using format_fn isn't the best idea here
77+ let text = (format_fn)(option);
78+ // TODO: using fuzzy_indices could give us the char idx for match highlighting
79+ matcher
80+ .fuzzy_match(&text, pattern)
81+ .map(|score| (index, score))
82+ }),
83+ );
84+ matches.sort_unstable_by_key(|(_, score)| -score);
85+
86+ // reset cursor position
87+ self.cursor = None;
88+ self.scroll = 0;
4389 }
4490
4591 pub fn move_up(&mut self) {
@@ -71,7 +117,11 @@ impl<T> Menu<T> {
71117 }
72118
73119 pub fn selection(&self) -> Option<&T> {
74− self.cursor.and_then(|cursor| self.options.get(cursor))
120+ self.cursor.and_then(|cursor| {
121+ self.matches
122+ .get(cursor)
123+ .map(|(index, _score)| &self.options[*index])
124+ })
75125 }
76126}
77127
@@ -186,7 +236,17 @@ impl<T> Component for Menu<T> {
186236 let selected = Style::default().fg(Color::Rgb(255, 255, 255));
187237
188238 let scroll = self.scroll;
189− let len = self.options.len();
239+
240+ let options: Vec<_> = self
241+ .matches
242+ .iter()
243+ .map(|(index, _score)| {
244+ // (index, self.options.get(*index).unwrap()) // get_unchecked
245+ &self.options[*index] // get_unchecked
246+ })
247+ .collect();
248+
249+ let len = options.len();
190250
191251 let win_height = area.height as usize;
192252
@@ -199,7 +259,7 @@ impl<T> Component for Menu<T> {
199259 let scroll_line = (win_height - scroll_height) * scroll
200260 / std::cmp::max(1, len.saturating_sub(win_height));
201261
202− for (i, option) in self.options[scroll..(scroll + win_height).min(len)]
262+ for (i, option) in options[scroll..(scroll + win_height).min(len)]
203263 .iter()
204264 .enumerate()
205265 {
helix-term/src/ui/mod.rs+2
@@ -1,3 +1,4 @@
1+mod completion;
12mod editor;
23mod markdown;
34mod menu;
@@ -6,6 +7,7 @@ mod popup;
67mod prompt;
78mod text;
89
10+pub use completion::Completion;
911pub use editor::EditorView;
1012pub use markdown::Markdown;
1113pub use menu::Menu;
helix-term/src/ui/popup.rs+9−5
@@ -15,17 +15,17 @@ use helix_view::Editor;
1515// TODO: share logic with Menu, it's essentially Popup(render_fn), but render fn needs to return
1616// a width/height hint. maybe Popup(Box<Component>)
1717
18−pub struct Popup {
19− contents: Box<dyn Component>,
18+pub struct Popup<T: Component> {
19+ contents: T,
2020 position: Option<Position>,
2121 size: (u16, u16),
2222 scroll: usize,
2323}
2424
25−impl Popup {
25+impl<T: Component> Popup<T> {
2626 // TODO: it's like a slimmed down picker, share code? (picker = menu + prompt with different
2727 // rendering)
28− pub fn new(contents: Box<dyn Component>) -> Self {
28+ pub fn new(contents: T) -> Self {
2929 Self {
3030 contents,
3131 position: None,
@@ -45,9 +45,13 @@ impl Popup {
4545 self.scroll = self.scroll.saturating_sub(offset);
4646 }
4747 }
48+
49+ pub fn contents(&mut self) -> &mut T {
50+ &mut self.contents
51+ }
4852}
4953
50−impl Component for Popup {
54+impl<T: Component> Component for Popup<T> {
5155 fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
5256 let key = match event {
5357 Event::Key(event) => event,