mirror of https://github.com/helix-editor/helix
discard stale completion requests
Completion requests are computed asynchronously to avoid common micro freezes while editing. This means that once a completion request completes, the state of the editor might have changed. Currently, there is a check to ensure we are still in insert mode. However, we also need to ensure that the view and document hasn't changed to avoid accidentally using a savepoint with the wrong view/document. Furthermore, the editor might request a new completion while the previous completion request hasn't complemented yet. This can lead to weird flickering or an outdated completion request replacing a newer completion that has already completed (the LSP server is not required to process completion requests in order). This change also needed to ensure determinism/linear ordering so that completion popup always correspond to the last completion request.pull/6242/head
parent
e8898fd9a8
commit
8cb7cdfd7a
|
@ -5,6 +5,7 @@ pub(crate) mod typed;
|
||||||
pub use dap::*;
|
pub use dap::*;
|
||||||
use helix_vcs::Hunk;
|
use helix_vcs::Hunk;
|
||||||
pub use lsp::*;
|
pub use lsp::*;
|
||||||
|
use tokio::sync::oneshot;
|
||||||
use tui::widgets::Row;
|
use tui::widgets::Row;
|
||||||
pub use typed::*;
|
pub use typed::*;
|
||||||
|
|
||||||
|
@ -4171,6 +4172,24 @@ pub fn completion(cx: &mut Context) {
|
||||||
None => return,
|
None => return,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// setup a chanel that allows the request to be canceled
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
// set completion_request so that this request can be canceled
|
||||||
|
// by setting completion_request, the old channel stored there is dropped
|
||||||
|
// and the associated request is automatically dropped
|
||||||
|
cx.editor.completion_request_handle = Some(tx);
|
||||||
|
let future = async move {
|
||||||
|
tokio::select! {
|
||||||
|
biased;
|
||||||
|
_ = rx => {
|
||||||
|
Ok(serde_json::Value::Null)
|
||||||
|
}
|
||||||
|
res = future => {
|
||||||
|
res
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let trigger_offset = cursor;
|
let trigger_offset = cursor;
|
||||||
|
|
||||||
// TODO: trigger_offset should be the cursor offset but we also need a starting offset from where we want to apply
|
// TODO: trigger_offset should be the cursor offset but we also need a starting offset from where we want to apply
|
||||||
|
@ -4183,11 +4202,19 @@ pub fn completion(cx: &mut Context) {
|
||||||
let start_offset = cursor.saturating_sub(offset);
|
let start_offset = cursor.saturating_sub(offset);
|
||||||
let savepoint = doc.savepoint(view);
|
let savepoint = doc.savepoint(view);
|
||||||
|
|
||||||
|
let trigger_doc = doc.id();
|
||||||
|
let trigger_view = view.id;
|
||||||
|
|
||||||
cx.callback(
|
cx.callback(
|
||||||
future,
|
future,
|
||||||
move |editor, compositor, response: Option<lsp::CompletionResponse>| {
|
move |editor, compositor, response: Option<lsp::CompletionResponse>| {
|
||||||
if editor.mode != Mode::Insert {
|
let (view, doc) = current_ref!(editor);
|
||||||
// we're not in insert mode anymore
|
// check if the completion request is stale.
|
||||||
|
//
|
||||||
|
// Completions are completed asynchrounsly and therefore the user could
|
||||||
|
//switch document/view or leave insert mode. In all of thoise cases the
|
||||||
|
// completion should be discarded
|
||||||
|
if editor.mode != Mode::Insert || view.id != trigger_view || doc.id() != trigger_doc {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -820,6 +820,7 @@ impl EditorView {
|
||||||
(Mode::Insert, Mode::Normal) => {
|
(Mode::Insert, Mode::Normal) => {
|
||||||
// if exiting insert mode, remove completion
|
// if exiting insert mode, remove completion
|
||||||
self.completion = None;
|
self.completion = None;
|
||||||
|
cxt.editor.completion_request_handle = None;
|
||||||
|
|
||||||
// TODO: Use an on_mode_change hook to remove signature help
|
// TODO: Use an on_mode_change hook to remove signature help
|
||||||
cxt.jobs.callback(async {
|
cxt.jobs.callback(async {
|
||||||
|
|
|
@ -31,7 +31,7 @@ use std::{
|
||||||
use tokio::{
|
use tokio::{
|
||||||
sync::{
|
sync::{
|
||||||
mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
|
mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender},
|
||||||
Notify, RwLock,
|
oneshot, Notify, RwLock,
|
||||||
},
|
},
|
||||||
time::{sleep, Duration, Instant, Sleep},
|
time::{sleep, Duration, Instant, Sleep},
|
||||||
};
|
};
|
||||||
|
@ -852,6 +852,14 @@ pub struct Editor {
|
||||||
/// avoid calculating the cursor position multiple
|
/// avoid calculating the cursor position multiple
|
||||||
/// times during rendering and should not be set by other functions.
|
/// times during rendering and should not be set by other functions.
|
||||||
pub cursor_cache: Cell<Option<Option<Position>>>,
|
pub cursor_cache: Cell<Option<Option<Position>>>,
|
||||||
|
/// When a new completion request is sent to the server old
|
||||||
|
/// unifinished request must be dropped. Each completion
|
||||||
|
/// request is associated with a channel that cancels
|
||||||
|
/// when the channel is dropped. That channel is stored
|
||||||
|
/// here. When a new completion request is sent this
|
||||||
|
/// field is set and any old requests are automatically
|
||||||
|
/// canceled as a result
|
||||||
|
pub completion_request_handle: Option<oneshot::Sender<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type RedrawHandle = (Arc<Notify>, Arc<RwLock<()>>);
|
pub type RedrawHandle = (Arc<Notify>, Arc<RwLock<()>>);
|
||||||
|
@ -950,6 +958,7 @@ impl Editor {
|
||||||
redraw_handle: Default::default(),
|
redraw_handle: Default::default(),
|
||||||
needs_redraw: false,
|
needs_redraw: false,
|
||||||
cursor_cache: Cell::new(None),
|
cursor_cache: Cell::new(None),
|
||||||
|
completion_request_handle: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue