diff options
Diffstat (limited to 'helix-term/src/commands.rs')
-rw-r--r-- | helix-term/src/commands.rs | 231 |
1 files changed, 152 insertions, 79 deletions
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 5fdf6a0a..dbdebce0 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -10,7 +10,7 @@ use helix_core::{ use once_cell::sync::Lazy; use crate::{ - compositor::{Callback, Compositor}, + compositor::{Callback, Component, Compositor}, ui::{self, Picker, Popup, Prompt, PromptEvent}, }; @@ -26,14 +26,20 @@ use crossterm::event::{KeyCode, KeyEvent}; use helix_lsp::lsp; +use crate::application::{LspCallbackWrapper, LspCallbacks}; + pub struct Context<'a> { pub count: usize, pub editor: &'a mut Editor, pub callback: Option<crate::compositor::Callback>, pub on_next_key_callback: Option<Box<dyn FnOnce(&mut Context, KeyEvent)>>, + pub callbacks: &'a mut LspCallbacks, } +use futures_util::FutureExt; +use std::future::Future; + impl<'a> Context<'a> { #[inline] pub fn view(&mut self) -> &mut View { @@ -47,7 +53,7 @@ impl<'a> Context<'a> { } /// Push a new component onto the compositor. - pub fn push_layer(&mut self, mut component: Box<dyn crate::compositor::Component>) { + pub fn push_layer(&mut self, mut component: Box<dyn Component>) { self.callback = Some(Box::new( |compositor: &mut Compositor, editor: &mut Editor| { let size = compositor.size(); @@ -65,6 +71,27 @@ impl<'a> Context<'a> { ) { self.on_next_key_callback = Some(Box::new(on_next_key_callback)); } + + #[inline] + pub fn callback<T, F>( + &mut self, + call: impl Future<Output = helix_lsp::Result<serde_json::Value>> + 'static + Send, + callback: F, + ) where + T: for<'de> serde::Deserialize<'de> + Send + 'static, + F: FnOnce(&mut Editor, &mut Compositor, T) + Send + 'static, + { + let callback = Box::pin(async move { + let json = call.await?; + let response = serde_json::from_value(json)?; + let call: LspCallbackWrapper = + Box::new(move |editor: &mut Editor, compositor: &mut Compositor| { + callback(editor, compositor, response) + }); + Ok(call) + }); + self.callbacks.push(callback); + } } /// A command is a function that takes the current state and a count, and does a side-effect on the @@ -1564,6 +1591,24 @@ pub fn save(cx: &mut Context) { } pub fn completion(cx: &mut Context) { + // trigger on trigger char, or if user calls it + // (or on word char typing??) + // after it's triggered, if response marked is_incomplete, update on every subsequent keypress + // + // lsp calls are done via a callback: it sends a request and doesn't block. + // when we get the response similarly to notification, trigger a call to the completion popup + // + // language_server.completion(params, |cx: &mut Context, _meta, response| { + // // called at response time + // // compositor, lookup completion layer + // // downcast dyn Component to Completion component + // // emit response to completion (completion.complete/handle(response)) + // }) + // async { + // let (response, callback) = response.await?; + // callback(response) + // } + let doc = cx.doc(); let language_server = match doc.language_server() { @@ -1576,91 +1621,119 @@ pub fn completion(cx: &mut Context) { // TODO: handle fails - let res = smol::block_on(language_server.completion(doc.identifier(), pos)).unwrap_or_default(); - - // TODO: if no completion, show some message or something - if !res.is_empty() { - // let snapshot = doc.state.clone(); - let mut menu = ui::Menu::new( - res, - |item| { - // format_fn - item.label.as_str().into() - - // TODO: use item.filter_text for filtering - }, - move |editor: &mut Editor, item, event| { - match event { - PromptEvent::Abort => { - // revert state - // let id = editor.view().doc; - // let doc = &mut editor.documents[id]; - // doc.state = snapshot.clone(); - } - PromptEvent::Validate => { - let id = editor.view().doc; - let doc = &mut editor.documents[id]; - - // revert state to what it was before the last update - // doc.state = snapshot.clone(); - - // extract as fn(doc, item): - - // TODO: need to apply without composing state... - // TODO: need to update lsp on accept/cancel by diffing the snapshot with - // the final state? - // -> on update simply update the snapshot, then on accept redo the call, - // finally updating doc.changes + notifying lsp. - // - // or we could simply use doc.undo + apply when changing between options - - // always present here - let item = item.unwrap(); - - use helix_lsp::{lsp, util}; - // determine what to insert: text_edit | insert_text | label - let edit = if let Some(edit) = &item.text_edit { - match edit { - lsp::CompletionTextEdit::Edit(edit) => edit.clone(), - lsp::CompletionTextEdit::InsertAndReplace(item) => { - unimplemented!("completion: insert_and_replace {:?}", item) + let res = smol::block_on(language_server.completion(doc.identifier(), pos)).unwrap(); + + cx.callback( + res, + |editor: &mut Editor, + compositor: &mut Compositor, + response: Option<lsp::CompletionResponse>| { + let items = match response { + Some(lsp::CompletionResponse::Array(items)) => items, + // TODO: do something with is_incomplete + Some(lsp::CompletionResponse::List(lsp::CompletionList { + is_incomplete: _is_incomplete, + items, + })) => items, + None => Vec::new(), + }; + + // TODO: if no completion, show some message or something + if !items.is_empty() { + // let snapshot = doc.state.clone(); + let mut menu = ui::Menu::new( + items, + |item| { + // format_fn + item.label.as_str().into() + + // TODO: use item.filter_text for filtering + }, + move |editor: &mut Editor, item, event| { + match event { + PromptEvent::Abort => { + // revert state + // let id = editor.view().doc; + // let doc = &mut editor.documents[id]; + // doc.state = snapshot.clone(); + } + PromptEvent::Validate => { + let id = editor.view().doc; + let doc = &mut editor.documents[id]; + + // revert state to what it was before the last update + // doc.state = snapshot.clone(); + + // extract as fn(doc, item): + + // TODO: need to apply without composing state... + // TODO: need to update lsp on accept/cancel by diffing the snapshot with + // the final state? + // -> on update simply update the snapshot, then on accept redo the call, + // finally updating doc.changes + notifying lsp. + // + // or we could simply use doc.undo + apply when changing between options + + // always present here + let item = item.unwrap(); + + use helix_lsp::{lsp, util}; + // determine what to insert: text_edit | insert_text | label + let edit = if let Some(edit) = &item.text_edit { + match edit { + lsp::CompletionTextEdit::Edit(edit) => edit.clone(), + lsp::CompletionTextEdit::InsertAndReplace(item) => { + unimplemented!( + "completion: insert_and_replace {:?}", + item + ) + } + } + } else { + item.insert_text.as_ref().unwrap_or(&item.label); + unimplemented!(); + // lsp::TextEdit::new(); TODO: calculate a TextEdit from insert_text + // and we insert at position. + }; + + // TODO: merge edit with additional_text_edits + if let Some(additional_edits) = &item.additional_text_edits { + if !additional_edits.is_empty() { + unimplemented!( + "completion: additional_text_edits: {:?}", + additional_edits + ); + } } + + let transaction = + util::generate_transaction_from_edits(doc.text(), vec![edit]); + doc.apply(&transaction); + // TODO: doc.append_changes_to_history(); if not in insert mode? } - } else { - item.insert_text.as_ref().unwrap_or(&item.label); - unimplemented!(); - // lsp::TextEdit::new(); TODO: calculate a TextEdit from insert_text - // and we insert at position. + _ => (), }; + }, + ); - // TODO: merge edit with additional_text_edits - if let Some(additional_edits) = &item.additional_text_edits { - if !additional_edits.is_empty() { - unimplemented!( - "completion: additional_text_edits: {:?}", - additional_edits - ); - } - } + let popup = Popup::new(Box::new(menu)); + let mut component: Box<dyn Component> = Box::new(popup); - // TODO: <-- if state has changed by further input, transaction will panic on len - let transaction = - util::generate_transaction_from_edits(doc.text(), vec![edit]); - doc.apply(&transaction); - // TODO: doc.append_changes_to_history(); if not in insert mode? - } - _ => (), - }; - }, - ); + // Server error: content modified - let popup = Popup::new(Box::new(menu)); - cx.push_layer(Box::new(popup)); + // TODO: this is shared with cx.push_layer + let size = compositor.size(); + // trigger required_size on init + component.required_size((size.width, size.height)); + compositor.push(component); + } + }, + ); - // TODO!: when iterating over items, show the docs in popup + // // TODO!: when iterating over items, show the docs in popup - // language server client needs to be accessible via a registry of some sort - } + // // language server client needs to be accessible via a registry of some sort + //} } pub fn hover(cx: &mut Context) { |