on 2158366 help sign in
helix/helix-term/src/ui/completion.rs
334 lines 13 kB history raw
1use crate::compositor::{Component, Context, EventResult};
2use crossterm::event::{Event, KeyCode, KeyEvent};
3use tui::buffer::Buffer as Surface;
5use std::borrow::Cow;
7use helix_core::Transaction;
8use helix_view::{graphics::Rect, Document, Editor, View};
10use crate::commands;
11use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
13use helix_lsp::{lsp, util};
14use lsp::CompletionItem;
16impl menu::Item for CompletionItem {
17 fn sort_text(&self) -> &str {
18 self.filter_text.as_ref().unwrap_or(&self.label).as_str()
19 }
21 fn filter_text(&self) -> &str {
22 self.filter_text.as_ref().unwrap_or(&self.label).as_str()
23 }
25 fn label(&self) -> &str {
26 self.label.as_str()
27 }
29 fn row(&self) -> menu::Row {
30 menu::Row::new(vec![
31 menu::Cell::from(self.label.as_str()),
32 menu::Cell::from(match self.kind {
33 Some(lsp::CompletionItemKind::Text) => "text",
34 Some(lsp::CompletionItemKind::Method) => "method",
35 Some(lsp::CompletionItemKind::Function) => "function",
36 Some(lsp::CompletionItemKind::Constructor) => "constructor",
37 Some(lsp::CompletionItemKind::Field) => "field",
38 Some(lsp::CompletionItemKind::Variable) => "variable",
39 Some(lsp::CompletionItemKind::Class) => "class",
40 Some(lsp::CompletionItemKind::Interface) => "interface",
41 Some(lsp::CompletionItemKind::Module) => "module",
42 Some(lsp::CompletionItemKind::Property) => "property",
43 Some(lsp::CompletionItemKind::Unit) => "unit",
44 Some(lsp::CompletionItemKind::Value) => "value",
45 Some(lsp::CompletionItemKind::Enum) => "enum",
46 Some(lsp::CompletionItemKind::Keyword) => "keyword",
47 Some(lsp::CompletionItemKind::Snippet) => "snippet",
48 Some(lsp::CompletionItemKind::Color) => "color",
49 Some(lsp::CompletionItemKind::File) => "file",
50 Some(lsp::CompletionItemKind::Reference) => "reference",
51 Some(lsp::CompletionItemKind::Folder) => "folder",
52 Some(lsp::CompletionItemKind::EnumMember) => "enum_member",
53 Some(lsp::CompletionItemKind::Constant) => "constant",
54 Some(lsp::CompletionItemKind::Struct) => "struct",
55 Some(lsp::CompletionItemKind::Event) => "event",
56 Some(lsp::CompletionItemKind::Operator) => "operator",
57 Some(lsp::CompletionItemKind::TypeParameter) => "type_param",
58 None => "",
59 }),
60 // self.detail.as_deref().unwrap_or("")
61 // self.label_details
62 // .as_ref()
63 // .or(self.detail())
64 // .as_str(),
65 ])
66 }
69/// Wraps a Menu.
70pub struct Completion {
71 popup: Popup<Menu<CompletionItem>>,
72 trigger_offset: usize,
73 // TODO: maintain a completioncontext with trigger kind & trigger char
76impl Completion {
77 pub fn new(
78 items: Vec<CompletionItem>,
79 offset_encoding: helix_lsp::OffsetEncoding,
80 trigger_offset: usize,
81 ) -> Self {
82 // let items: Vec<CompletionItem> = Vec::new();
83 let menu = Menu::new(items, move |editor: &mut Editor, item, event| {
84 fn item_to_transaction(
85 doc: &Document,
86 view: &View,
87 item: &CompletionItem,
88 offset_encoding: helix_lsp::OffsetEncoding,
89 ) -> Transaction {
90 if let Some(edit) = &item.text_edit {
91 let edit = match edit {
92 lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
93 lsp::CompletionTextEdit::InsertAndReplace(item) => {
94 unimplemented!("completion: insert_and_replace {:?}", item)
95 }
96 };
97 util::generate_transaction_from_edits(
98 doc.text(),
99 vec![edit],
100 offset_encoding, // TODO: should probably transcode in Client
101 )
102 } else {
103 let text = item.insert_text.as_ref().unwrap_or(&item.label);
104 let cursor = doc
105 .selection(view.id)
106 .primary()
107 .cursor(doc.text().slice(..));
108 Transaction::change(
109 doc.text(),
110 vec![(cursor, cursor, Some(text.as_str().into()))].into_iter(),
111 )
112 }
113 }
115 match event {
116 PromptEvent::Abort => {}
117 PromptEvent::Update => {
118 let (view, doc) = current!(editor);
120 // always present here
121 let item = item.unwrap();
123 // if more text was entered, remove it
124 // TODO: ideally to undo we should keep the last completion tx revert, and map it over new changes
125 let cursor = doc
126 .selection(view.id)
127 .primary()
128 .cursor(doc.text().slice(..));
129 if trigger_offset < cursor {
130 let remove = Transaction::change(
131 doc.text(),
132 vec![(trigger_offset, cursor, None)].into_iter(),
133 );
134 doc.apply(&remove, view.id);
135 }
137 let transaction = item_to_transaction(doc, view, item, offset_encoding);
138 doc.apply(&transaction, view.id);
139 }
140 PromptEvent::Validate => {
141 let (view, doc) = current!(editor);
143 // always present here
144 let item = item.unwrap();
146 // if more text was entered, remove it
147 // TODO: ideally to undo we should keep the last completion tx revert, and map it over new changes
148 let cursor = doc
149 .selection(view.id)
150 .primary()
151 .cursor(doc.text().slice(..));
152 if trigger_offset < cursor {
153 let remove = Transaction::change(
154 doc.text(),
155 vec![(trigger_offset, cursor, None)].into_iter(),
156 );
157 doc.apply(&remove, view.id);
158 }
160 let transaction = item_to_transaction(doc, view, item, offset_encoding);
161 doc.apply(&transaction, view.id);
163 if let Some(additional_edits) = &item.additional_text_edits {
164 // gopls uses this to add extra imports
165 if !additional_edits.is_empty() {
166 let transaction = util::generate_transaction_from_edits(
167 doc.text(),
168 additional_edits.clone(),
169 offset_encoding, // TODO: should probably transcode in Client
170 );
171 doc.apply(&transaction, view.id);
172 }
173 }
174 }
175 };
176 });
177 let popup = Popup::new(menu);
178 Self {
179 popup,
180 trigger_offset,
181 }
182 }
184 pub fn update(&mut self, cx: &mut commands::Context) {
185 // recompute menu based on matches
186 let menu = self.popup.contents_mut();
187 let (view, doc) = current!(cx.editor);
189 // cx.hooks()
190 // cx.add_hook(enum type, ||)
191 // cx.trigger_hook(enum type, &str, ...) <-- there has to be enough to identify doc/view
192 // callback with editor & compositor
193 //
194 // trigger_hook sends event into channel, that's consumed in the global loop and
195 // triggers all registered callbacks
196 // TODO: hooks should get processed immediately so maybe do it after select!(), before
197 // looping?
199 let cursor = doc
200 .selection(view.id)
201 .primary()
202 .cursor(doc.text().slice(..));
203 if self.trigger_offset <= cursor {
204 let fragment = doc.text().slice(self.trigger_offset..cursor);
205 let text = Cow::from(fragment);
206 // TODO: logic is same as ui/picker
207 menu.score(&text);
208 }
209 }
211 pub fn is_empty(&self) -> bool {
212 self.popup.contents().is_empty()
213 }
216// need to:
217// - trigger on the right trigger char
218// - detect previous open instance and recycle
219// - update after input, but AFTER the document has changed
220// - if no more matches, need to auto close
221//
222// missing bits:
223// - a more robust hook system: emit to a channel, process in main loop
224// - a way to find specific layers in compositor
225// - components register for hooks, then unregister when terminated
226// ... since completion is a special case, maybe just build it into doc/render?
228impl Component for Completion {
229 fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
230 // let the Editor handle Esc instead
231 if let Event::Key(KeyEvent {
232 code: KeyCode::Esc, ..
233 }) = event
234 {
235 return EventResult::Ignored;
236 }
237 self.popup.handle_event(event, cx)
238 }
240 fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
241 self.popup.required_size(viewport)
242 }
244 fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
245 self.popup.render(area, surface, cx);
247 // if we have a selection, render a markdown popup on top/below with info
248 if let Some(option) = self.popup.contents().selection() {
249 // need to render:
250 // option.detail
251 // ---
252 // option.documentation
254 let (view, doc) = current!(cx.editor);
255 let language = doc
256 .language()
257 .and_then(|scope| scope.strip_prefix("source."))
258 .unwrap_or("");
259 let cursor_pos = doc
260 .selection(view.id)
261 .primary()
262 .cursor(doc.text().slice(..));
263 let cursor_pos = (helix_core::coords_at_pos(doc.text().slice(..), cursor_pos).row
264 - view.offset.row) as u16;
266 let mut doc = match &option.documentation {
267 Some(lsp::Documentation::String(contents))
268 | Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
269 kind: lsp::MarkupKind::PlainText,
270 value: contents,
271 })) => {
272 // TODO: convert to wrapped text
273 Markdown::new(
274 format!(
275 "```{}\n{}\n```\n{}",
276 language,
277 option.detail.as_deref().unwrap_or_default(),
278 contents.clone()
279 ),
280 cx.editor.syn_loader.clone(),
281 )
282 }
283 Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
284 kind: lsp::MarkupKind::Markdown,
285 value: contents,
286 })) => {
287 // TODO: set language based on doc scope
288 Markdown::new(
289 format!(
290 "```{}\n{}\n```\n{}",
291 language,
292 option.detail.as_deref().unwrap_or_default(),
293 contents.clone()
294 ),
295 cx.editor.syn_loader.clone(),
296 )
297 }
298 None if option.detail.is_some() => {
299 // TODO: copied from above
301 // TODO: set language based on doc scope
302 Markdown::new(
303 format!(
304 "```{}\n{}\n```",
305 language,
306 option.detail.as_deref().unwrap_or_default(),
307 ),
308 cx.editor.syn_loader.clone(),
309 )
310 }
311 None => return,
312 };
314 let half = area.height / 2;
315 let height = 15.min(half);
316 // we want to make sure the cursor is visible (not hidden behind the documentation)
317 let y = if cursor_pos + area.y
318 >= (cx.editor.tree.area().height - height - 2/* statusline + commandline */)
319 {
320 0
321 } else {
322 // -2 to subtract command line + statusline. a bit of a hack, because of splits.
323 area.height.saturating_sub(height).saturating_sub(2)
324 };
326 let area = Rect::new(0, y, area.width, height);
328 // clear area
329 let background = cx.editor.theme.get("ui.popup");
330 surface.clear_with(area, background);
331 doc.render(area, surface, cx);
332 }
333 }