diff --git a/Cargo.lock b/Cargo.lock index 3f4d3b8c..b9e14f87 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -549,6 +549,8 @@ dependencies = [ "num_cpus", "once_cell", "pulldown-cmark", + "serde", + "serde_json", "smol", "smol-timeout", "toml", diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index 7c9de90c..1d84c4f3 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -1,13 +1,12 @@ use crate::{ transport::{Payload, Transport}, - Call, Error, + Call, Error, Result, }; -type Result = core::result::Result; - use helix_core::{ChangeSet, Rope}; // use std::collections::HashMap; +use std::future::Future; use std::sync::atomic::{AtomicU64, Ordering}; use jsonrpc_core as jsonrpc; @@ -96,6 +95,21 @@ impl Client { where R::Params: serde::Serialize, R::Result: core::fmt::Debug, // TODO: temporary + { + // a future that resolves into the response + let future = self.call::(params).await?; + let json = future.await?; + let response = serde_json::from_value(json)?; + Ok(response) + } + + /// Execute a RPC request on the language server. + pub async fn call( + &self, + params: R::Params, + ) -> Result>> + where + R::Params: serde::Serialize, { let params = serde_json::to_value(params)?; @@ -119,15 +133,15 @@ impl Client { use smol_timeout::TimeoutExt; use std::time::Duration; - let response = match rx.recv().timeout(Duration::from_secs(2)).await { - Some(response) => response, - None => return Err(Error::Timeout), - } - .map_err(|e| Error::Other(e.into()))??; + let future = async move { + rx.recv() + .timeout(Duration::from_secs(2)) + .await + .ok_or(Error::Timeout)? // return Timeout + .map_err(|e| Error::Other(e.into()))? + }; - let response = serde_json::from_value(response)?; - - Ok(response) + Ok(future) } /// Send a RPC notification to the language server. @@ -447,7 +461,8 @@ impl Client { &self, text_document: lsp::TextDocumentIdentifier, position: lsp::Position, - ) -> Result> { + ) -> Result>> { + // ) -> Result> { let params = lsp::CompletionParams { text_document_position: lsp::TextDocumentPositionParams { text_document, @@ -464,19 +479,7 @@ impl Client { // lsp::CompletionContext { trigger_kind: , trigger_character: Some(), } }; - let response = self.request::(params).await?; - - 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(), - }; - - Ok(items) + self.call::(params).await } pub async fn text_document_signature_help( diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index e7fe816a..89054345 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -8,6 +8,8 @@ pub use lsp_types as lsp; pub use client::Client; pub use lsp::{Position, Url}; +pub type Result = core::result::Result; + use helix_core::syntax::LanguageConfiguration; use thiserror::Error; diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index 2f3aa384..a68b9d7d 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -44,3 +44,6 @@ pulldown-cmark = { version = "0.8", default-features = false } # config toml = "0.5" + +serde_json = "1.0" +serde = { version = "1.0", features = ["derive"] } 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(); + let res = smol::block_on(language_server.completion(doc.identifier(), pos)).unwrap(); - // 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() + 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: 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]; + // 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() - // revert state to what it was before the last update - // doc.state = snapshot.clone(); + // 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]; - // extract as fn(doc, item): + // revert state to what it was before the last update + // doc.state = snapshot.clone(); - // 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 + // extract as fn(doc, item): - // always present here - let item = item.unwrap(); + // 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 - 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) + // 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, };