mirror of
https://github.com/helix-editor/helix.git
synced 2024-11-22 09:26:19 +04:00
wip: Async async. Delay response handling with a callback.
This commit is contained in:
parent
ad3325db8e
commit
2a3910c1d9
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -549,6 +549,8 @@ dependencies = [
|
||||
"num_cpus",
|
||||
"once_cell",
|
||||
"pulldown-cmark",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"smol",
|
||||
"smol-timeout",
|
||||
"toml",
|
||||
|
@ -1,13 +1,12 @@
|
||||
use crate::{
|
||||
transport::{Payload, Transport},
|
||||
Call, Error,
|
||||
Call, Error, Result,
|
||||
};
|
||||
|
||||
type Result<T> = core::result::Result<T, Error>;
|
||||
|
||||
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 @@ pub async fn request<R: lsp::request::Request>(&self, params: R::Params) -> Resu
|
||||
where
|
||||
R::Params: serde::Serialize,
|
||||
R::Result: core::fmt::Debug, // TODO: temporary
|
||||
{
|
||||
// a future that resolves into the response
|
||||
let future = self.call::<R>(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<R: lsp::request::Request>(
|
||||
&self,
|
||||
params: R::Params,
|
||||
) -> Result<impl Future<Output = Result<Value>>>
|
||||
where
|
||||
R::Params: serde::Serialize,
|
||||
{
|
||||
let params = serde_json::to_value(params)?;
|
||||
|
||||
@ -119,15 +133,15 @@ pub async fn request<R: lsp::request::Request>(&self, params: R::Params) -> Resu
|
||||
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 @@ pub async fn completion(
|
||||
&self,
|
||||
text_document: lsp::TextDocumentIdentifier,
|
||||
position: lsp::Position,
|
||||
) -> Result<Vec<lsp::CompletionItem>> {
|
||||
) -> Result<impl Future<Output = Result<Value>>> {
|
||||
// ) -> Result<Vec<lsp::CompletionItem>> {
|
||||
let params = lsp::CompletionParams {
|
||||
text_document_position: lsp::TextDocumentPositionParams {
|
||||
text_document,
|
||||
@ -464,19 +479,7 @@ pub async fn completion(
|
||||
// lsp::CompletionContext { trigger_kind: , trigger_character: Some(), }
|
||||
};
|
||||
|
||||
let response = self.request::<lsp::request::Completion>(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::<lsp::request::Completion>(params).await
|
||||
}
|
||||
|
||||
pub async fn text_document_signature_help(
|
||||
|
@ -8,6 +8,8 @@
|
||||
pub use client::Client;
|
||||
pub use lsp::{Position, Url};
|
||||
|
||||
pub type Result<T> = core::result::Result<T, Error>;
|
||||
|
||||
use helix_core::syntax::LanguageConfiguration;
|
||||
|
||||
use thiserror::Error;
|
||||
|
@ -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"] }
|
||||
|
@ -24,11 +24,23 @@
|
||||
|
||||
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 @@ pub fn new(mut args: Args, executor: &'static smol::Executor<'static>) -> Result
|
||||
editor,
|
||||
|
||||
executor,
|
||||
callbacks: FuturesUnordered::new(),
|
||||
};
|
||||
|
||||
Ok(app)
|
||||
@ -59,10 +72,12 @@ fn render(&mut self) {
|
||||
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 @@ pub async fn event_loop(&mut self) {
|
||||
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
|
||||
|
@ -10,7 +10,7 @@
|
||||
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 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 @@ pub fn doc(&mut self) -> &mut Document {
|
||||
}
|
||||
|
||||
/// 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 @@ pub fn on_next_key(
|
||||
) {
|
||||
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();
|
||||
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<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: 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<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) {
|
||||
|
@ -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 {
|
||||
|
@ -439,6 +439,7 @@ fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
|
||||
editor: &mut cx.editor,
|
||||
count: 1,
|
||||
callback: None,
|
||||
callbacks: cx.callbacks,
|
||||
on_next_key_callback: None,
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user