1use crate::compositor::{Component, Context, EventResult};
2use crossterm::event::{Event, KeyCode, KeyEvent};
3use tui::buffer::Buffer as Surface;
7use helix_core::Transaction;
8use helix_view::{graphics::Rect, Document, Editor, View};
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()
21 fn filter_text(&self) -> &str {
22 self.filter_text.as_ref().unwrap_or(&self.label).as_str()
25 fn label(&self) -> &str {
29 fn row(&self) -> menu::Row {
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",
60 // self.detail.as_deref().unwrap_or("")
70pub struct Completion {
71 popup: Popup<Menu<CompletionItem>>,
72 trigger_offset: usize,
73 // TODO: maintain a completioncontext with trigger kind & trigger char
78 items: Vec<CompletionItem>,
79 offset_encoding: helix_lsp::OffsetEncoding,
80 trigger_offset: usize,
82 // let items: Vec<CompletionItem> = Vec::new();
83 let menu = Menu::new(items, move |editor: &mut Editor, item, event| {
84 fn item_to_transaction(
87 item: &CompletionItem,
88 offset_encoding: helix_lsp::OffsetEncoding,
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)
97 util::generate_transaction_from_edits(
100 offset_encoding, // TODO: should probably transcode in Client
103 let text = item.insert_text.as_ref().unwrap_or(&item.label);
107 .cursor(doc.text().slice(..));
110 vec![(cursor, cursor, Some(text.as_str().into()))].into_iter(),
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
128 .cursor(doc.text().slice(..));
129 if trigger_offset < cursor {
130 let remove = Transaction::change(
132 vec![(trigger_offset, cursor, None)].into_iter(),
134 doc.apply(&remove, view.id);
137 let transaction = item_to_transaction(doc, view, item, offset_encoding);
138 doc.apply(&transaction, view.id);
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
151 .cursor(doc.text().slice(..));
152 if trigger_offset < cursor {
153 let remove = Transaction::change(
155 vec![(trigger_offset, cursor, None)].into_iter(),
157 doc.apply(&remove, view.id);
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(
168 additional_edits.clone(),
169 offset_encoding, // TODO: should probably transcode in Client
171 doc.apply(&transaction, view.id);
177 let popup = Popup::new(menu);
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);
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
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
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
211 pub fn is_empty(&self) -> bool {
212 self.popup.contents().is_empty()
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
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, ..
235 return EventResult::Ignored;
237 self.popup.handle_event(event, cx)
240 fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
241 self.popup.required_size(viewport)
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() {
252 // option.documentation
254 let (view, doc) = current!(cx.editor);
257 .and_then(|scope| scope.strip_prefix("source."))
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,
272 // TODO: convert to wrapped text
275 "```{}\n{}\n```\n{}",
277 option.detail.as_deref().unwrap_or_default(),
280 cx.editor.syn_loader.clone(),
283 Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
284 kind: lsp::MarkupKind::Markdown,
287 // TODO: set language based on doc scope
290 "```{}\n{}\n```\n{}",
292 option.detail.as_deref().unwrap_or_default(),
295 cx.editor.syn_loader.clone(),
298 None if option.detail.is_some() => {
299 // TODO: copied from above
301 // TODO: set language based on doc scope
306 option.detail.as_deref().unwrap_or_default(),
308 cx.editor.syn_loader.clone(),
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 */)
322 // -2 to subtract command line + statusline. a bit of a hack, because of splits.
323 area.height.saturating_sub(height).saturating_sub(2)
326 let area = Rect::new(0, y, area.width, height);
329 let background = cx.editor.theme.get("ui.popup");
330 surface.clear_with(area, background);
331 doc.render(area, surface, cx);
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list