aboutsummaryrefslogtreecommitdiff
path: root/helix-term/src
diff options
context:
space:
mode:
authorBlaž Hrastnik2021-03-26 07:02:13 +0000
committerBlaž Hrastnik2021-03-26 08:12:45 +0000
commit2a3910c1d9f2b05fe5bc0610e6a83e6dabe13b71 (patch)
tree5e2c94eec0c168d14b1478a07110230b3be1c7fe /helix-term/src
parentad3325db8e6dce3a10b9f8e0319ab9814c7ade1b (diff)
wip: Async async. Delay response handling with a callback.
Diffstat (limited to 'helix-term/src')
-rw-r--r--helix-term/src/application.rs29
-rw-r--r--helix-term/src/commands.rs231
-rw-r--r--helix-term/src/compositor.rs3
-rw-r--r--helix-term/src/ui/editor.rs1
4 files changed, 185 insertions, 79 deletions
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<T> = Pin<Box<dyn Future<Output = T> + Send>>;
+pub type LspCallback =
+ BoxFuture<Result<Box<dyn FnOnce(&mut Editor, &mut Compositor) + Send>, anyhow::Error>>;
+
+pub type LspCallbacks = FuturesUnordered<LspCallback>;
+pub type LspCallbackWrapper = Box<dyn FnOnce(&mut Editor, &mut Compositor) + Send>;
+
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<Result<LspCallbackWrapper, anyhow::Error>>,
+ ) {
+ 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<Result<Event, crossterm::ErrorKind>>) {
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<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) {
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<usize>,
+ 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,
};