From 2a3910c1d9f2b05fe5bc0610e6a83e6dabe13b71 Mon Sep 17 00:00:00 2001 From: Blaž Hrastnik Date: Fri, 26 Mar 2021 16:02:13 +0900 Subject: wip: Async async. Delay response handling with a callback. --- helix-term/src/application.rs | 29 ++++++ helix-term/src/commands.rs | 231 +++++++++++++++++++++++++++--------------- helix-term/src/compositor.rs | 3 + helix-term/src/ui/editor.rs | 1 + 4 files changed, 185 insertions(+), 79 deletions(-) (limited to 'helix-term/src') diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index dcc6433b..b7f88aaa 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -24,11 +24,23 @@ use crossterm::{ use tui::layout::Rect; +// use futures_util::future::BoxFuture; +use futures_util::stream::FuturesUnordered; +use std::pin::Pin; + +type BoxFuture = Pin + Send>>; +pub type LspCallback = + BoxFuture, anyhow::Error>>; + +pub type LspCallbacks = FuturesUnordered; +pub type LspCallbackWrapper = Box; + pub struct Application { compositor: Compositor, editor: Editor, executor: &'static smol::Executor<'static>, + callbacks: LspCallbacks, } impl Application { @@ -50,6 +62,7 @@ impl Application { editor, executor, + callbacks: FuturesUnordered::new(), }; Ok(app) @@ -59,10 +72,12 @@ impl Application { let executor = &self.executor; let editor = &mut self.editor; let compositor = &mut self.compositor; + let callbacks = &mut self.callbacks; let mut cx = crate::compositor::Context { editor, executor, + callbacks, scroll: None, }; @@ -87,14 +102,28 @@ impl Application { call = self.editor.language_servers.incoming.next().fuse() => { self.handle_language_server_message(call).await } + callback = self.callbacks.next().fuse() => { + self.handle_language_server_callback(callback) + } } } } + pub fn handle_language_server_callback( + &mut self, + callback: Option>, + ) { + if let Some(Ok(callback)) = callback { + // TODO: handle Err() + callback(&mut self.editor, &mut self.compositor); + self.render(); + } + } pub fn handle_terminal_events(&mut self, event: Option>) { let mut cx = crate::compositor::Context { editor: &mut self.editor, executor: &self.executor, + callbacks: &mut self.callbacks, scroll: None, }; // Handle key events 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, pub on_next_key_callback: Option>, + 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) { + pub fn push_layer(&mut self, mut component: Box) { 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( + &mut self, + call: impl Future> + '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| { + 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 = 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) { diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs index 023f9b49..bd27f138 100644 --- a/helix-term/src/compositor.rs +++ b/helix-term/src/compositor.rs @@ -25,10 +25,13 @@ pub enum EventResult { use helix_view::{Editor, View}; +use crate::application::LspCallbacks; + pub struct Context<'a> { pub editor: &'a mut Editor, pub executor: &'static smol::Executor<'static>, pub scroll: Option, + pub callbacks: &'a mut LspCallbacks, } pub trait Component { diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index ba9eda42..f55411b8 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -439,6 +439,7 @@ impl Component for EditorView { editor: &mut cx.editor, count: 1, callback: None, + callbacks: cx.callbacks, on_next_key_callback: None, }; -- cgit v1.2.3-70-g09d2