diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index f8a888e7a058a..d5c6fa67a0cb5 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2587,13 +2587,11 @@ pub mod insert { super::completion(cx); } - fn language_server_completion(cx: &mut Context, ch: char) { + pub fn is_server_trigger_char(doc: &Document, ch: char) -> bool { use helix_lsp::lsp; - // if ch matches completion char, trigger completion - let doc = doc_mut!(cx.editor); let language_server = match doc.language_server() { Some(language_server) => language_server, - None => return, + None => return false, }; let capabilities = language_server.capabilities(); @@ -2603,12 +2601,25 @@ pub mod insert { .. }) = &capabilities.completion_provider { - // TODO: what if trigger is multiple chars long if triggers.iter().any(|trigger| trigger.contains(ch)) { - cx.editor.clear_idle_timer(); - super::completion(cx); + return true; } } + false + } + + fn language_server_completion(cx: &mut Context, ch: char) { + use helix_core::chars::char_is_word; + + // if ch matches completion char, trigger completion + let doc = doc_mut!(cx.editor); + if char_is_word(ch) && doc.savepoint.is_none() { + cx.editor.reset_idle_timer(); + return; + } + if is_server_trigger_char(doc, ch) { + cx.editor.reset_idle_timer_zero(); + } } fn signature_help(cx: &mut Context, ch: char) { @@ -3486,6 +3497,12 @@ pub fn completion(cx: &mut Context) { let pos = pos_to_lsp_pos(doc.text(), cursor, offset_encoding); let future = language_server.completion(doc.identifier(), pos, None); + let future = async move { + match future.await { + Ok(v) => Ok(v), + Err(_) => Ok(serde_json::Value::Null), + } + }; let trigger_offset = cursor; @@ -3499,53 +3516,74 @@ pub fn completion(cx: &mut Context) { let start_offset = cursor.saturating_sub(offset); let prefix = text.slice(start_offset..cursor).to_string(); + doc.savepoint(); + let trigger_version = doc.version(); cx.callback( future, move |editor: &mut Editor, compositor: &mut Compositor, response: Option| { - let doc = doc!(editor); - if doc.mode() != Mode::Insert { - // we're not in insert mode anymore - return; - } + let get_items = || { + let doc = doc!(editor); + if doc.mode() != Mode::Insert { + // we're not in insert mode anymore + return None; + } + match &doc.savepoint { + Some((current, _)) if *current == trigger_version => {} + _ => return None, + }; + if response.is_none() { + if doc.version() != trigger_version { + editor.reset_idle_timer_zero(); + } + return None; + } - let mut 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(), - }; + let mut 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 => return None, + }; - if !prefix.is_empty() { - items = items - .into_iter() - .filter(|item| { - item.filter_text - .as_ref() - .unwrap_or(&item.label) - .starts_with(&prefix) - }) - .collect(); - } + if !prefix.is_empty() { + items = items + .into_iter() + .filter(|item| { + item.filter_text + .as_ref() + .unwrap_or(&item.label) + .starts_with(&prefix) + }) + .collect(); + }; - if items.is_empty() { - // editor.set_error("No completion available"); - return; - } + if items.is_empty() { + // editor.set_error("No completion available".to_string()); + return None; + } + Some(items) + }; let size = compositor.size(); let ui = compositor.find::().unwrap(); - ui.set_completion( - editor, - items, - offset_encoding, - start_offset, - trigger_offset, - size, - ); + match get_items() { + Some(items) => ui.set_completion( + editor, + items, + offset_encoding, + start_offset, + trigger_offset, + size, + ), + None => { + ui.completion = None; + doc_mut!(editor).savepoint = None; + } + } }, ); } diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 31a9bfc86b5e1..17142d018dd25 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -824,9 +824,6 @@ impl EditorView { return; } - // Immediately initialize a savepoint - doc_mut!(editor).savepoint(); - editor.last_completion = None; self.last_insert.1.push(InsertEvent::TriggerCompletion); @@ -845,23 +842,52 @@ impl EditorView { } pub fn handle_idle_timeout(&mut self, cx: &mut crate::compositor::Context) -> EventResult { - if self.completion.is_some() - || !cx.editor.config.auto_completion - || doc!(cx.editor).mode != Mode::Insert - { + use commands::insert::is_server_trigger_char; + use helix_core::chars::char_is_word; + + let (view, doc) = current!(cx.editor); + if doc.mode != Mode::Insert || !cx.editor.config.auto_completion { return EventResult::Ignored(None); } - let mut cx = commands::Context { + let is_trigger = || -> bool { + let text = doc.text().slice(..); + let cursor = doc.selection(view.id).primary().cursor(text); + + let mut iter = text.chars_at(cursor); + iter.reverse(); + let last_char = match iter.next() { + Some(c) => c, + None => return false, + }; + if is_server_trigger_char(doc, last_char) { + return true; + } + if !char_is_word(last_char) { + return false; + } + for _ in 1..cx.editor.config.completion_trigger_len { + match iter.next() { + Some(c) if char_is_word(c) || is_server_trigger_char(doc, c) => {} + _ => return false, + } + } + true + }; + + if !is_trigger() { + return EventResult::Ignored(None); + } + + self.clear_completion(cx.editor); + commands::completion(&mut commands::Context { register: None, editor: cx.editor, jobs: cx.jobs, count: None, callback: None, on_next_key_callback: None, - }; - crate::commands::insert::idle_completion(&mut cx); - + }); EventResult::Consumed(None) } } @@ -1094,7 +1120,7 @@ impl Component for EditorView { EventResult::Consumed(None) } Event::Key(key) => { - cx.editor.reset_idle_timer(); + cx.editor.clear_idle_timer(); let mut key = KeyEvent::from(key); canonicalize_key(&mut key); @@ -1146,7 +1172,8 @@ impl Component for EditorView { if let Some(completion) = &mut self.completion { completion.update(&mut cx); if completion.is_empty() { - self.clear_completion(cx.editor); + self.completion = None; + doc_mut!(cx.editor).savepoint = None; } } } @@ -1193,7 +1220,7 @@ impl Component for EditorView { } (Mode::Insert, Mode::Normal) => { // if exiting insert mode, remove completion - self.completion = None; + self.clear_completion(cx.editor); } _ => (), } diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 671ceb75f84ff..e09fb254dced9 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -112,7 +112,7 @@ pub struct Document { // be more troublesome. pub history: Cell, - pub savepoint: Option, + pub savepoint: Option<(i32, Transaction)>, last_saved_revision: usize, version: i32, // should be usize? @@ -649,7 +649,8 @@ impl Document { if self.savepoint.is_some() { take_with(&mut self.savepoint, |prev_revert| { let revert = transaction.invert(&old_doc); - Some(revert.compose(prev_revert.unwrap())) + let (version, prev_revert) = prev_revert.unwrap(); + Some((version, revert.compose(prev_revert))) }); } @@ -737,11 +738,11 @@ impl Document { } pub fn savepoint(&mut self) { - self.savepoint = Some(Transaction::new(self.text())); + self.savepoint = Some((self.version, Transaction::new(self.text()))); } pub fn restore(&mut self, view_id: ViewId) { - if let Some(revert) = self.savepoint.take() { + if let Some((_, revert)) = self.savepoint.take() { self.apply(&revert, view_id); } } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index adf0cdf335a70..94d89c725402a 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -393,6 +393,10 @@ impl Editor { .reset(Instant::now() + self.config.idle_timeout); } + pub fn reset_idle_timer_zero(&mut self) { + self.idle_timer.as_mut().reset(Instant::now()); + } + pub fn clear_status(&mut self) { self.status_msg = None; }