correctly handle completion rerequest

pull/16/head
Pascal Kuthe 1 year ago committed by Blaž Hrastnik
parent 91da0dc172
commit 5406e9f629

@ -33,7 +33,7 @@ use helix_core::{
use helix_view::{ use helix_view::{
clipboard::ClipboardType, clipboard::ClipboardType,
document::{FormatterError, Mode, SCRATCH_BUFFER_NAME}, document::{FormatterError, Mode, SCRATCH_BUFFER_NAME},
editor::{Action, Motion}, editor::{Action, CompleteAction, Motion},
info::Info, info::Info,
input::KeyEvent, input::KeyEvent,
keyboard::KeyCode, keyboard::KeyCode,
@ -4254,7 +4254,12 @@ pub fn completion(cx: &mut Context) {
iter.reverse(); iter.reverse();
let offset = iter.take_while(|ch| chars::char_is_word(*ch)).count(); let offset = iter.take_while(|ch| chars::char_is_word(*ch)).count();
let start_offset = cursor.saturating_sub(offset); let start_offset = cursor.saturating_sub(offset);
let savepoint = doc.savepoint(view); let savepoint = if let Some(CompleteAction::Selected { savepoint }) = &cx.editor.last_completion
{
savepoint.clone()
} else {
doc.savepoint(view)
};
let trigger_doc = doc.id(); let trigger_doc = doc.id();
let trigger_view = view.id; let trigger_view = view.id;

@ -209,14 +209,27 @@ impl Completion {
let (view, doc) = current!(editor); let (view, doc) = current!(editor);
// if more text was entered, remove it
doc.restore(view, &savepoint);
match event { match event {
PromptEvent::Abort => { PromptEvent::Abort => {}
editor.last_completion = None;
}
PromptEvent::Update => { PromptEvent::Update => {
// Update creates "ghost" transactiosn which are not send to the
// lsp server to avoid messing up rerequesting completions. Once a
// completion has been selected (with) tab it's always accepted whenever anything
// is typed. The only way to avoid that is to explicitly abort the completion
// with esc/c-c. This will remove the "ghost" transaction.
//
// The ghost transaction is modeled with a transaction that is not send to the LS.
// (apply_temporary) and a savepoint. It's extremly important this savepoint is restored
// (also without sending the transaction to the LS) *before any further transaction is applied*.
// Otherwise incremental sync breaks (since the state of the LS doesn't match the state the transaction
// is applied to).
if editor.last_completion.is_none() {
editor.last_completion = Some(CompleteAction::Selected {
savepoint: doc.savepoint(view),
})
}
// if more text was entered, remove it
doc.restore(view, &savepoint, false);
// always present here // always present here
let item = item.unwrap(); let item = item.unwrap();
@ -229,19 +242,20 @@ impl Completion {
true, true,
replace_mode, replace_mode,
); );
doc.apply_temporary(&transaction, view.id);
// initialize a savepoint
doc.apply(&transaction, view.id);
editor.last_completion = Some(CompleteAction {
trigger_offset,
changes: completion_changes(&transaction, trigger_offset),
});
} }
PromptEvent::Validate => { PromptEvent::Validate => {
if let Some(CompleteAction::Selected { savepoint }) =
editor.last_completion.take()
{
doc.restore(view, &savepoint, false);
}
// always present here // always present here
let item = item.unwrap(); let item = item.unwrap();
// if more text was entered, remove it
doc.restore(view, &savepoint, true);
let transaction = item_to_transaction( let transaction = item_to_transaction(
doc, doc,
view.id, view.id,
@ -251,10 +265,9 @@ impl Completion {
false, false,
replace_mode, replace_mode,
); );
doc.apply(&transaction, view.id); doc.apply(&transaction, view.id);
editor.last_completion = Some(CompleteAction { editor.last_completion = Some(CompleteAction::Applied {
trigger_offset, trigger_offset,
changes: completion_changes(&transaction, trigger_offset), changes: completion_changes(&transaction, trigger_offset),
}); });
@ -270,7 +283,6 @@ impl Completion {
} else { } else {
Self::resolve_completion_item(doc, item.clone()) Self::resolve_completion_item(doc, item.clone())
}; };
if let Some(additional_edits) = resolved_item if let Some(additional_edits) = resolved_item
.as_ref() .as_ref()
.and_then(|item| item.additional_text_edits.as_ref()) .and_then(|item| item.additional_text_edits.as_ref())

@ -19,7 +19,7 @@ use helix_core::{
syntax::{self, HighlightEvent}, syntax::{self, HighlightEvent},
text_annotations::TextAnnotations, text_annotations::TextAnnotations,
unicode::width::UnicodeWidthStr, unicode::width::UnicodeWidthStr,
visual_offset_from_block, Position, Range, Selection, Transaction, visual_offset_from_block, Change, Position, Range, Selection, Transaction,
}; };
use helix_view::{ use helix_view::{
document::{Mode, SavePoint, SCRATCH_BUFFER_NAME}, document::{Mode, SavePoint, SCRATCH_BUFFER_NAME},
@ -48,7 +48,10 @@ pub struct EditorView {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum InsertEvent { pub enum InsertEvent {
Key(KeyEvent), Key(KeyEvent),
CompletionApply(CompleteAction), CompletionApply {
trigger_offset: usize,
changes: Vec<Change>,
},
TriggerCompletion, TriggerCompletion,
RequestCompletion, RequestCompletion,
} }
@ -813,7 +816,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.clear_completion(cxt.editor);
cxt.editor.completion_request_handle = 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
@ -891,22 +894,25 @@ impl EditorView {
for key in self.last_insert.1.clone() { for key in self.last_insert.1.clone() {
match key { match key {
InsertEvent::Key(key) => self.insert_mode(cxt, key), InsertEvent::Key(key) => self.insert_mode(cxt, key),
InsertEvent::CompletionApply(compl) => { InsertEvent::CompletionApply {
trigger_offset,
changes,
} => {
let (view, doc) = current!(cxt.editor); let (view, doc) = current!(cxt.editor);
if let Some(last_savepoint) = last_savepoint.as_deref() { if let Some(last_savepoint) = last_savepoint.as_deref() {
doc.restore(view, last_savepoint); doc.restore(view, last_savepoint, true);
} }
let text = doc.text().slice(..); let text = doc.text().slice(..);
let cursor = doc.selection(view.id).primary().cursor(text); let cursor = doc.selection(view.id).primary().cursor(text);
let shift_position = let shift_position =
|pos: usize| -> usize { pos + cursor - compl.trigger_offset }; |pos: usize| -> usize { pos + cursor - trigger_offset };
let tx = Transaction::change( let tx = Transaction::change(
doc.text(), doc.text(),
compl.changes.iter().cloned().map(|(start, end, t)| { changes.iter().cloned().map(|(start, end, t)| {
(shift_position(start), shift_position(end), t) (shift_position(start), shift_position(end), t)
}), }),
); );
@ -979,6 +985,21 @@ impl EditorView {
pub fn clear_completion(&mut self, editor: &mut Editor) { pub fn clear_completion(&mut self, editor: &mut Editor) {
self.completion = None; self.completion = None;
if let Some(last_completion) = editor.last_completion.take() {
match last_completion {
CompleteAction::Applied {
trigger_offset,
changes,
} => self.last_insert.1.push(InsertEvent::CompletionApply {
trigger_offset,
changes,
}),
CompleteAction::Selected { savepoint } => {
let (view, doc) = current!(editor);
doc.restore(view, &savepoint, false);
}
}
}
// Clear any savepoints // Clear any savepoints
editor.clear_idle_timer(); // don't retrigger editor.clear_idle_timer(); // don't retrigger
@ -1265,12 +1286,22 @@ impl Component for EditorView {
jobs: cx.jobs, jobs: cx.jobs,
scroll: None, scroll: None,
}; };
completion.handle_event(event, &mut cx)
};
if let EventResult::Consumed(callback) = res { if let EventResult::Consumed(callback) =
consumed = true; completion.handle_event(event, &mut cx)
{
consumed = true;
Some(callback)
} else if let EventResult::Consumed(callback) =
completion.handle_event(&Event::Key(key!(Enter)), &mut cx)
{
Some(callback)
} else {
None
}
};
if let Some(callback) = res {
if callback.is_some() { if callback.is_some() {
// assume close_fn // assume close_fn
self.clear_completion(cx.editor); self.clear_completion(cx.editor);
@ -1286,10 +1317,6 @@ impl Component for EditorView {
// if completion didn't take the event, we pass it onto commands // if completion didn't take the event, we pass it onto commands
if !consumed { if !consumed {
if let Some(compl) = cx.editor.last_completion.take() {
self.last_insert.1.push(InsertEvent::CompletionApply(compl));
}
self.insert_mode(&mut cx, key); self.insert_mode(&mut cx, key);
// record last_insert key // record last_insert key

@ -1034,7 +1034,12 @@ impl Document {
} }
/// Apply a [`Transaction`] to the [`Document`] to change its text. /// Apply a [`Transaction`] to the [`Document`] to change its text.
fn apply_impl(&mut self, transaction: &Transaction, view_id: ViewId) -> bool { fn apply_impl(
&mut self,
transaction: &Transaction,
view_id: ViewId,
emit_lsp_notification: bool,
) -> bool {
use helix_core::Assoc; use helix_core::Assoc;
let old_doc = self.text().clone(); let old_doc = self.text().clone();
@ -1130,25 +1135,31 @@ impl Document {
apply_inlay_hint_changes(padding_after_inlay_hints); apply_inlay_hint_changes(padding_after_inlay_hints);
} }
// emit lsp notification if emit_lsp_notification {
if let Some(language_server) = self.language_server() { // emit lsp notification
let notify = language_server.text_document_did_change( if let Some(language_server) = self.language_server() {
self.versioned_identifier(), let notify = language_server.text_document_did_change(
&old_doc, self.versioned_identifier(),
self.text(), &old_doc,
changes, self.text(),
); changes,
);
if let Some(notify) = notify { if let Some(notify) = notify {
tokio::spawn(notify); tokio::spawn(notify);
}
} }
} }
} }
success success
} }
/// Apply a [`Transaction`] to the [`Document`] to change its text. fn apply_inner(
pub fn apply(&mut self, transaction: &Transaction, view_id: ViewId) -> bool { &mut self,
transaction: &Transaction,
view_id: ViewId,
emit_lsp_notification: bool,
) -> bool {
// store the state just before any changes are made. This allows us to undo to the // store the state just before any changes are made. This allows us to undo to the
// state just before a transaction was applied. // state just before a transaction was applied.
if self.changes.is_empty() && !transaction.changes().is_empty() { if self.changes.is_empty() && !transaction.changes().is_empty() {
@ -1158,7 +1169,7 @@ impl Document {
}); });
} }
let success = self.apply_impl(transaction, view_id); let success = self.apply_impl(transaction, view_id, emit_lsp_notification);
if !transaction.changes().is_empty() { if !transaction.changes().is_empty() {
// Compose this transaction with the previous one // Compose this transaction with the previous one
@ -1168,12 +1179,23 @@ impl Document {
} }
success success
} }
/// Apply a [`Transaction`] to the [`Document`] to change its text.
pub fn apply(&mut self, transaction: &Transaction, view_id: ViewId) -> bool {
self.apply_inner(transaction, view_id, true)
}
/// Apply a [`Transaction`] to the [`Document`] to change its text.
/// without notifying the language servers. This is useful for temporary transactions
/// that must not influence the server.
pub fn apply_temporary(&mut self, transaction: &Transaction, view_id: ViewId) -> bool {
self.apply_inner(transaction, view_id, false)
}
fn undo_redo_impl(&mut self, view: &mut View, undo: bool) -> bool { fn undo_redo_impl(&mut self, view: &mut View, undo: bool) -> bool {
let mut history = self.history.take(); let mut history = self.history.take();
let txn = if undo { history.undo() } else { history.redo() }; let txn = if undo { history.undo() } else { history.redo() };
let success = if let Some(txn) = txn { let success = if let Some(txn) = txn {
self.apply_impl(txn, view.id) self.apply_impl(txn, view.id, true)
} else { } else {
false false
}; };
@ -1213,7 +1235,7 @@ impl Document {
savepoint savepoint
} }
pub fn restore(&mut self, view: &mut View, savepoint: &SavePoint) { pub fn restore(&mut self, view: &mut View, savepoint: &SavePoint, emit_lsp_notification: bool) {
assert_eq!( assert_eq!(
savepoint.view, view.id, savepoint.view, view.id,
"Savepoint must not be used with a different view!" "Savepoint must not be used with a different view!"
@ -1228,7 +1250,7 @@ impl Document {
let savepoint_ref = self.savepoints.remove(savepoint_idx); let savepoint_ref = self.savepoints.remove(savepoint_idx);
let mut revert = savepoint.revert.lock(); let mut revert = savepoint.revert.lock();
self.apply(&revert, view.id); self.apply_inner(&revert, view.id, emit_lsp_notification);
*revert = Transaction::new(self.text()).with_selection(self.selection(view.id).clone()); *revert = Transaction::new(self.text()).with_selection(self.selection(view.id).clone());
self.savepoints.push(savepoint_ref) self.savepoints.push(savepoint_ref)
} }
@ -1241,7 +1263,7 @@ impl Document {
}; };
let mut success = false; let mut success = false;
for txn in txns { for txn in txns {
if self.apply_impl(&txn, view.id) { if self.apply_impl(&txn, view.id, true) {
success = true; success = true;
} }
} }

@ -1,7 +1,7 @@
use crate::{ use crate::{
align_view, align_view,
clipboard::{get_clipboard_provider, ClipboardProvider}, clipboard::{get_clipboard_provider, ClipboardProvider},
document::{DocumentSavedEventFuture, DocumentSavedEventResult, Mode}, document::{DocumentSavedEventFuture, DocumentSavedEventResult, Mode, SavePoint},
graphics::{CursorKind, Rect}, graphics::{CursorKind, Rect},
info::Info, info::Info,
input::KeyEvent, input::KeyEvent,
@ -906,9 +906,14 @@ enum ThemeAction {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct CompleteAction { pub enum CompleteAction {
pub trigger_offset: usize, Applied {
pub changes: Vec<Change>, trigger_offset: usize,
changes: Vec<Change>,
},
/// A savepoint of the currently active completion. The completion
/// MUST be restored before sending any event to the LSP
Selected { savepoint: Arc<SavePoint> },
} }
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]

Loading…
Cancel
Save