Skip to content

Commit a705b70

Browse files
committed
Refactored the Completion Menu
- Added a CompletionItem for the Completion menu to (potentially) support more than just LSP CompletionItems - Added a method 'add_completion_items' to asynchronously add more options if the completion menu is already open
1 parent c8ebd4c commit a705b70

File tree

4 files changed

+115
-87
lines changed

4 files changed

+115
-87
lines changed

helix-term/src/commands.rs

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,10 @@ use movement::Movement;
4444
use crate::{
4545
args,
4646
compositor::{self, Component, Compositor},
47-
ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent},
47+
ui::{
48+
self, menu::Item, overlay::overlayed, CompletionItem, FilePicker, Picker, Popup, Prompt,
49+
PromptEvent,
50+
},
4851
};
4952

5053
use crate::job::{self, Job, Jobs};
@@ -3615,6 +3618,8 @@ pub fn completion(cx: &mut Context) {
36153618
None => return,
36163619
};
36173620

3621+
let language_server_id = language_server.id();
3622+
36183623
let offset_encoding = language_server.offset_encoding();
36193624
let text = doc.text().slice(..);
36203625
let cursor = doc.selection(view.id).primary().cursor(text);
@@ -3652,17 +3657,19 @@ pub fn completion(cx: &mut Context) {
36523657
items,
36533658
})) => items,
36543659
None => Vec::new(),
3655-
};
3660+
}
3661+
.into_iter()
3662+
.map(|item| CompletionItem::LSP {
3663+
language_server_id,
3664+
item,
3665+
offset_encoding,
3666+
})
3667+
.collect::<Vec<_>>();
36563668

36573669
if !prefix.is_empty() {
36583670
items = items
36593671
.into_iter()
3660-
.filter(|item| {
3661-
item.filter_text
3662-
.as_ref()
3663-
.unwrap_or(&item.label)
3664-
.starts_with(&prefix)
3665-
})
3672+
.filter(|item| item.filter_text().starts_with(&prefix))
36663673
.collect();
36673674
}
36683675

@@ -3672,14 +3679,7 @@ pub fn completion(cx: &mut Context) {
36723679
}
36733680
let size = compositor.size();
36743681
let ui = compositor.find::<ui::EditorView>().unwrap();
3675-
ui.set_completion(
3676-
editor,
3677-
items,
3678-
offset_encoding,
3679-
start_offset,
3680-
trigger_offset,
3681-
size,
3682-
);
3682+
ui.set_completion(editor, items, start_offset, trigger_offset, size);
36833683
},
36843684
);
36853685
}

helix-term/src/ui/completion.rs

Lines changed: 94 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -11,59 +11,81 @@ use helix_view::{graphics::Rect, Document, Editor};
1111
use crate::commands;
1212
use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
1313

14-
use helix_lsp::{lsp, util};
15-
use lsp::CompletionItem;
14+
use helix_lsp::{lsp, util, OffsetEncoding};
15+
16+
#[derive(Clone)]
17+
pub enum CompletionItem {
18+
LSP {
19+
language_server_id: usize,
20+
item: lsp::CompletionItem,
21+
offset_encoding: OffsetEncoding,
22+
},
23+
}
1624

1725
impl menu::Item for CompletionItem {
1826
fn sort_text(&self) -> &str {
19-
self.filter_text.as_ref().unwrap_or(&self.label).as_str()
27+
match self {
28+
CompletionItem::LSP { item, .. } => {
29+
item.filter_text.as_ref().unwrap_or(&item.label).as_str()
30+
}
31+
}
2032
}
2133

2234
fn filter_text(&self) -> &str {
23-
self.filter_text.as_ref().unwrap_or(&self.label).as_str()
35+
match self {
36+
CompletionItem::LSP { item, .. } => {
37+
item.filter_text.as_ref().unwrap_or(&item.label).as_str()
38+
}
39+
}
2440
}
2541

2642
fn label(&self) -> &str {
27-
self.label.as_str()
43+
match self {
44+
CompletionItem::LSP { item, .. } => item.label.as_str(),
45+
}
2846
}
2947

3048
fn row(&self) -> menu::Row {
3149
menu::Row::new(vec![
32-
menu::Cell::from(self.label.as_str()),
33-
menu::Cell::from(match self.kind {
34-
Some(lsp::CompletionItemKind::TEXT) => "text",
35-
Some(lsp::CompletionItemKind::METHOD) => "method",
36-
Some(lsp::CompletionItemKind::FUNCTION) => "function",
37-
Some(lsp::CompletionItemKind::CONSTRUCTOR) => "constructor",
38-
Some(lsp::CompletionItemKind::FIELD) => "field",
39-
Some(lsp::CompletionItemKind::VARIABLE) => "variable",
40-
Some(lsp::CompletionItemKind::CLASS) => "class",
41-
Some(lsp::CompletionItemKind::INTERFACE) => "interface",
42-
Some(lsp::CompletionItemKind::MODULE) => "module",
43-
Some(lsp::CompletionItemKind::PROPERTY) => "property",
44-
Some(lsp::CompletionItemKind::UNIT) => "unit",
45-
Some(lsp::CompletionItemKind::VALUE) => "value",
46-
Some(lsp::CompletionItemKind::ENUM) => "enum",
47-
Some(lsp::CompletionItemKind::KEYWORD) => "keyword",
48-
Some(lsp::CompletionItemKind::SNIPPET) => "snippet",
49-
Some(lsp::CompletionItemKind::COLOR) => "color",
50-
Some(lsp::CompletionItemKind::FILE) => "file",
51-
Some(lsp::CompletionItemKind::REFERENCE) => "reference",
52-
Some(lsp::CompletionItemKind::FOLDER) => "folder",
53-
Some(lsp::CompletionItemKind::ENUM_MEMBER) => "enum_member",
54-
Some(lsp::CompletionItemKind::CONSTANT) => "constant",
55-
Some(lsp::CompletionItemKind::STRUCT) => "struct",
56-
Some(lsp::CompletionItemKind::EVENT) => "event",
57-
Some(lsp::CompletionItemKind::OPERATOR) => "operator",
58-
Some(lsp::CompletionItemKind::TYPE_PARAMETER) => "type_param",
59-
Some(kind) => unimplemented!("{:?}", kind),
60-
None => "",
61-
}),
62-
// self.detail.as_deref().unwrap_or("")
63-
// self.label_details
64-
// .as_ref()
65-
// .or(self.detail())
66-
// .as_str(),
50+
menu::Cell::from(self.label()),
51+
match self {
52+
CompletionItem::LSP { item, .. } => {
53+
menu::Cell::from(match item.kind {
54+
Some(lsp::CompletionItemKind::TEXT) => "text",
55+
Some(lsp::CompletionItemKind::METHOD) => "method",
56+
Some(lsp::CompletionItemKind::FUNCTION) => "function",
57+
Some(lsp::CompletionItemKind::CONSTRUCTOR) => "constructor",
58+
Some(lsp::CompletionItemKind::FIELD) => "field",
59+
Some(lsp::CompletionItemKind::VARIABLE) => "variable",
60+
Some(lsp::CompletionItemKind::CLASS) => "class",
61+
Some(lsp::CompletionItemKind::INTERFACE) => "interface",
62+
Some(lsp::CompletionItemKind::MODULE) => "module",
63+
Some(lsp::CompletionItemKind::PROPERTY) => "property",
64+
Some(lsp::CompletionItemKind::UNIT) => "unit",
65+
Some(lsp::CompletionItemKind::VALUE) => "value",
66+
Some(lsp::CompletionItemKind::ENUM) => "enum",
67+
Some(lsp::CompletionItemKind::KEYWORD) => "keyword",
68+
Some(lsp::CompletionItemKind::SNIPPET) => "snippet",
69+
Some(lsp::CompletionItemKind::COLOR) => "color",
70+
Some(lsp::CompletionItemKind::FILE) => "file",
71+
Some(lsp::CompletionItemKind::REFERENCE) => "reference",
72+
Some(lsp::CompletionItemKind::FOLDER) => "folder",
73+
Some(lsp::CompletionItemKind::ENUM_MEMBER) => "enum_member",
74+
Some(lsp::CompletionItemKind::CONSTANT) => "constant",
75+
Some(lsp::CompletionItemKind::STRUCT) => "struct",
76+
Some(lsp::CompletionItemKind::EVENT) => "event",
77+
Some(lsp::CompletionItemKind::OPERATOR) => "operator",
78+
Some(lsp::CompletionItemKind::TYPE_PARAMETER) => "type_param",
79+
Some(kind) => unimplemented!("{:?}", kind),
80+
None => "",
81+
})
82+
// self.detail.as_deref().unwrap_or("")
83+
// self.label_details
84+
// .as_ref()
85+
// .or(self.detail())
86+
// .as_str(),
87+
}
88+
},
6789
])
6890
}
6991
}
@@ -81,18 +103,24 @@ impl Completion {
81103
pub fn new(
82104
editor: &Editor,
83105
items: Vec<CompletionItem>,
84-
offset_encoding: helix_lsp::OffsetEncoding,
85106
start_offset: usize,
86107
trigger_offset: usize,
87108
) -> Self {
88109
let menu = Menu::new(items, move |editor: &mut Editor, item, event| {
89110
fn item_to_transaction(
90111
doc: &Document,
91112
item: &CompletionItem,
92-
offset_encoding: helix_lsp::OffsetEncoding,
93113
start_offset: usize,
94114
trigger_offset: usize,
95115
) -> Transaction {
116+
// for now only LSP support
117+
let (item, offset_encoding) = match item {
118+
CompletionItem::LSP {
119+
item,
120+
offset_encoding,
121+
..
122+
} => (item, *offset_encoding),
123+
};
96124
let transaction = if let Some(edit) = &item.text_edit {
97125
let edit = match edit {
98126
lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
@@ -142,13 +170,7 @@ impl Completion {
142170
// always present here
143171
let item = item.unwrap();
144172

145-
let transaction = item_to_transaction(
146-
doc,
147-
item,
148-
offset_encoding,
149-
start_offset,
150-
trigger_offset,
151-
);
173+
let transaction = item_to_transaction(doc, item, start_offset, trigger_offset);
152174

153175
// initialize a savepoint
154176
doc.savepoint();
@@ -163,13 +185,7 @@ impl Completion {
163185
// always present here
164186
let item = item.unwrap();
165187

166-
let transaction = item_to_transaction(
167-
doc,
168-
item,
169-
offset_encoding,
170-
start_offset,
171-
trigger_offset,
172-
);
188+
let transaction = item_to_transaction(doc, item, start_offset, trigger_offset);
173189

174190
doc.apply(&transaction, view.id);
175191

@@ -178,22 +194,34 @@ impl Completion {
178194
changes: completion_changes(&transaction, trigger_offset),
179195
});
180196

197+
let (lsp_item, offset_encoding, language_server_id) = match item {
198+
CompletionItem::LSP {
199+
item,
200+
offset_encoding,
201+
language_server_id,
202+
} => (item, *offset_encoding, *language_server_id),
203+
};
204+
181205
// apply additional edits, mostly used to auto import unqualified types
182-
let resolved_item = if item
206+
let resolved_item = if lsp_item
183207
.additional_text_edits
184208
.as_ref()
185209
.map(|edits| !edits.is_empty())
186210
.unwrap_or(false)
187211
{
188212
None
189213
} else {
190-
Self::resolve_completion_item(doc, item.clone())
214+
let language_server = editor
215+
.language_servers
216+
.get_by_id(language_server_id)
217+
.unwrap();
218+
Self::resolve_completion_item(language_server, lsp_item.clone())
191219
};
192220

193221
if let Some(additional_edits) = resolved_item
194222
.as_ref()
195223
.and_then(|item| item.additional_text_edits.as_ref())
196-
.or(item.additional_text_edits.as_ref())
224+
.or(lsp_item.additional_text_edits.as_ref())
197225
{
198226
if !additional_edits.is_empty() {
199227
let transaction = util::generate_transaction_from_edits(
@@ -221,11 +249,9 @@ impl Completion {
221249
}
222250

223251
fn resolve_completion_item(
224-
doc: &Document,
252+
language_server: &helix_lsp::Client,
225253
completion_item: lsp::CompletionItem,
226-
) -> Option<CompletionItem> {
227-
// TODO support multiple language servers instead of taking the first language server
228-
let language_server = doc.language_servers().first().map(|l| *l)?;
254+
) -> Option<lsp::CompletionItem> {
229255
let completion_resolve_provider = language_server
230256
.capabilities()
231257
.completion_provider
@@ -246,6 +272,10 @@ impl Completion {
246272
}
247273
}
248274

275+
pub fn add_completion_items(&mut self, items: Vec<CompletionItem>) {
276+
self.popup.contents_mut().add_options(items);
277+
}
278+
249279
pub fn recompute_filter(&mut self, editor: &Editor) {
250280
// recompute menu based on matches
251281
let menu = self.popup.contents_mut();
@@ -306,7 +336,7 @@ impl Component for Completion {
306336
self.popup.render(area, surface, cx);
307337

308338
// if we have a selection, render a markdown popup on top/below with info
309-
if let Some(option) = self.popup.contents().selection() {
339+
if let Some(CompletionItem::LSP { item: option, .. }) = self.popup.contents().selection() {
310340
// need to render:
311341
// option.detail
312342
// ---

helix-term/src/ui/editor.rs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use crate::{
33
compositor::{Component, Context, EventResult},
44
key,
55
keymap::{KeymapResult, Keymaps},
6-
ui::{Completion, ProgressSpinners},
6+
ui::{Completion, CompletionItem, ProgressSpinners},
77
};
88

99
use helix_core::{
@@ -914,14 +914,12 @@ impl EditorView {
914914
pub fn set_completion(
915915
&mut self,
916916
editor: &mut Editor,
917-
items: Vec<helix_lsp::lsp::CompletionItem>,
918-
offset_encoding: helix_lsp::OffsetEncoding,
917+
items: Vec<CompletionItem>,
919918
start_offset: usize,
920919
trigger_offset: usize,
921920
size: Rect,
922921
) {
923-
let mut completion =
924-
Completion::new(editor, items, offset_encoding, start_offset, trigger_offset);
922+
let mut completion = Completion::new(editor, items, start_offset, trigger_offset);
925923

926924
if completion.is_empty() {
927925
// skip if we got no completion results

helix-term/src/ui/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ mod prompt;
1010
mod spinner;
1111
mod text;
1212

13-
pub use completion::Completion;
13+
pub use completion::{Completion, CompletionItem};
1414
pub use editor::EditorView;
1515
pub use markdown::Markdown;
1616
pub use menu::Menu;
17-
pub use picker::{FileLocation, FilePicker, Picker};
1817
pub use overlay::Overlay;
18+
pub use picker::{FileLocation, FilePicker, Picker};
1919
pub use popup::Popup;
2020
pub use prompt::{Prompt, PromptEvent};
2121
pub use spinner::{ProgressSpinners, Spinner};

0 commit comments

Comments
 (0)