From f99bea404f43ea0e373fd9fe54616d3282e8364b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Sun, 22 Aug 2021 15:28:45 +0900 Subject: [PATCH 1/8] idle timer wip --- helix-term/src/application.rs | 5 +++++ helix-term/src/ui/editor.rs | 1 + helix-view/src/editor.rs | 20 +++++++++++++++++++- 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 6206e6f2..dbd8755d 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -199,6 +199,11 @@ impl Application { self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback); self.render(); } + _ = &mut self.editor.idle_timer => { + self.editor.clear_idle_timer(); + println!("idle!") + // idle timeout + } } } } diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 0605e2c7..aa2d6636 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -901,6 +901,7 @@ impl Component for EditorView { EventResult::Consumed(None) } Event::Key(key) => { + cxt.editor.reset_idle_timer(); let mut key = KeyEvent::from(key); canonicalize_key(&mut key); // clear status diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index b08a2df2..5362acc8 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -9,10 +9,12 @@ use crate::{ use futures_util::future; use std::{ path::{Path, PathBuf}, + pin::Pin, sync::Arc, - time::Duration, }; +use tokio::time::{sleep, Duration, Instant, Sleep}; + use slotmap::SlotMap; use anyhow::Error; @@ -91,6 +93,8 @@ pub struct Editor { pub status_msg: Option<(String, Severity)>, pub config: Config, + + pub idle_timer: Pin>, } #[derive(Debug, Copy, Clone)] @@ -125,10 +129,24 @@ impl Editor { registers: Registers::default(), clipboard_provider: get_clipboard_provider(), status_msg: None, + idle_timer: Box::pin(sleep(Duration::from_millis(500))), config, } } + pub fn clear_idle_timer(&mut self) { + // equivalent to internal Instant::far_future() (30 years) + self.idle_timer + .as_mut() + .reset(Instant::now() + Duration::from_secs(86400 * 365 * 30)); + } + + pub fn reset_idle_timer(&mut self) { + self.idle_timer + .as_mut() + .reset(Instant::now() + Duration::from_millis(500)); + } + pub fn clear_status(&mut self) { self.status_msg = None; } From 40abec80e1062503d70055ed9e968cd4b31411a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Thu, 26 Aug 2021 11:14:46 +0900 Subject: [PATCH 2/8] Experiment with autocompletion on idle --- helix-term/src/application.rs | 38 +++++++++++++++++++++++++++++++++-- helix-term/src/commands.rs | 2 +- helix-term/src/ui/editor.rs | 2 +- 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index dbd8755d..ab268041 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -200,9 +200,9 @@ impl Application { self.render(); } _ = &mut self.editor.idle_timer => { + // idle timeout self.editor.clear_idle_timer(); - println!("idle!") - // idle timeout + self.handle_idle_timeout(); } } } @@ -233,6 +233,40 @@ impl Application { } } + pub fn handle_idle_timeout(&mut self) { + use helix_view::document::Mode; + use crate::commands::{Context, completion}; + + + if doc_mut!(self.editor).mode != Mode::Insert { + return; + } + let editor_view = self + .compositor + .find(std::any::type_name::()) + .expect("expected at least one EditorView"); + let editor_view = editor_view + .as_any_mut() + .downcast_mut::() + .unwrap(); + + if editor_view.completion.is_some() { + return; + } + + let mut cx = Context { + selected_register: helix_view::RegisterSelection::default(), + editor: &mut self.editor, + jobs: &mut self.jobs, + count: None, + callback: None, + on_next_key_callback: None, + }; + completion(&mut cx); + // TODO: scan backwards for trigger and filter the box + self.render(); + } + pub fn handle_terminal_events(&mut self, event: Option>) { let mut cx = crate::compositor::Context { editor: &mut self.editor, diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index ec76c81d..1dbb5616 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -4045,7 +4045,7 @@ fn remove_primary_selection(cx: &mut Context) { doc.set_selection(view.id, selection); } -fn completion(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 diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index aa2d6636..ee3b3c65 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -33,7 +33,7 @@ pub struct EditorView { keymaps: Keymaps, on_next_key: Option>, last_insert: (commands::Command, Vec), - completion: Option, + pub(crate) completion: Option, spinners: ProgressSpinners, autoinfo: Option, } From 66f26e82ceaf4b81dfeb429dcb840a3242e8f254 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Fri, 27 Aug 2021 10:44:12 +0900 Subject: [PATCH 3/8] Filter the initial completion --- helix-term/src/application.rs | 11 ++++++----- helix-term/src/commands.rs | 17 ++++++++++++----- helix-term/src/ui/completion.rs | 25 +++++++++++++++++++------ helix-term/src/ui/editor.rs | 5 ++++- helix-view/src/macros.rs | 16 ++++++++++++++++ 5 files changed, 57 insertions(+), 17 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index ab268041..79921268 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -234,9 +234,8 @@ impl Application { } pub fn handle_idle_timeout(&mut self) { + use crate::commands::{completion, Context}; use helix_view::document::Mode; - use crate::commands::{Context, completion}; - if doc_mut!(self.editor).mode != Mode::Insert { return; @@ -254,6 +253,8 @@ impl Application { return; } + // TODO: if completion window was closed with enter (and no selection) we shouldn't retrigger + let mut cx = Context { selected_register: helix_view::RegisterSelection::default(), editor: &mut self.editor, @@ -262,9 +263,9 @@ impl Application { callback: None, on_next_key_callback: None, }; - completion(&mut cx); - // TODO: scan backwards for trigger and filter the box - self.render(); + completion(&mut cx); + // TODO: scan backwards for trigger and filter the box + self.render(); } pub fn handle_terminal_events(&mut self, event: Option>) { diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 1dbb5616..3798c99a 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -4090,10 +4090,8 @@ pub fn completion(cx: &mut Context) { }; let offset_encoding = language_server.offset_encoding(); - let cursor = doc - .selection(view.id) - .primary() - .cursor(doc.text().slice(..)); + let text = doc.text().slice(..); + let cursor = doc.selection(view.id).primary().cursor(text); let pos = pos_to_lsp_pos(doc.text(), cursor, offset_encoding); @@ -4101,6 +4099,15 @@ pub fn completion(cx: &mut Context) { 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 + // completion filtering. For example logger.te| should filter the initial suggestion list with "te". + + use helix_core::chars; + let mut iter = text.chars_at(cursor); + iter.reverse(); + let offset = iter.take_while(|ch| chars::char_is_word(*ch)).count(); + let start_offset = cursor.saturating_sub(offset); + cx.callback( future, move |editor: &mut Editor, @@ -4131,7 +4138,7 @@ pub fn completion(cx: &mut Context) { .find(std::any::type_name::()) .unwrap(); if let Some(ui) = ui.as_any_mut().downcast_mut::() { - ui.set_completion(items, offset_encoding, trigger_offset, size); + ui.set_completion(editor, items, offset_encoding, start_offset, trigger_offset, size); }; }, ); diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 6c9e3a80..72b03274 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -69,14 +69,17 @@ impl menu::Item for CompletionItem { /// Wraps a Menu. pub struct Completion { popup: Popup>, + start_offset: usize, trigger_offset: usize, // TODO: maintain a completioncontext with trigger kind & trigger char } impl Completion { pub fn new( + editor: &Editor, items: Vec, offset_encoding: helix_lsp::OffsetEncoding, + start_offset: usize, trigger_offset: usize, ) -> Self { // let items: Vec = Vec::new(); @@ -175,16 +178,22 @@ impl Completion { }; }); let popup = Popup::new(menu); - Self { + let mut completion = Self { popup, + start_offset, trigger_offset, - } + }; + + // need to recompute immediately in case start_offset != trigger_offset + completion.recompute_filter(editor); + + completion } - pub fn update(&mut self, cx: &mut commands::Context) { + pub fn recompute_filter(&mut self, editor: &Editor) { // recompute menu based on matches let menu = self.popup.contents_mut(); - let (view, doc) = current!(cx.editor); + let (view, doc) = current_ref!(editor); // cx.hooks() // cx.add_hook(enum type, ||) @@ -200,14 +209,18 @@ impl Completion { .selection(view.id) .primary() .cursor(doc.text().slice(..)); - if self.trigger_offset <= cursor { - let fragment = doc.text().slice(self.trigger_offset..cursor); + if self.start_offset <= cursor { + let fragment = doc.text().slice(self.start_offset..cursor); let text = Cow::from(fragment); // TODO: logic is same as ui/picker menu.score(&text); } } + pub fn update(&mut self, cx: &mut commands::Context) { + self.recompute_filter(cx.editor) + } + pub fn is_empty(&self) -> bool { self.popup.contents().is_empty() } diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index ee3b3c65..fda6a6d3 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -721,12 +721,15 @@ impl EditorView { pub fn set_completion( &mut self, + editor: &Editor, items: Vec, offset_encoding: helix_lsp::OffsetEncoding, + start_offset: usize, trigger_offset: usize, size: Rect, ) { - let mut completion = Completion::new(items, offset_encoding, trigger_offset); + let mut completion = + Completion::new(editor, items, offset_encoding, start_offset, trigger_offset); // TODO : propagate required size on resize to completion too completion.required_size((size.width, size.height)); self.completion = Some(completion); diff --git a/helix-view/src/macros.rs b/helix-view/src/macros.rs index c9a04270..0bebd02f 100644 --- a/helix-view/src/macros.rs +++ b/helix-view/src/macros.rs @@ -44,3 +44,19 @@ macro_rules! view { $( $editor ).+ .tree.get($( $editor ).+ .tree.focus) }}; } + +#[macro_export] +macro_rules! doc { + ( $( $editor:ident ).+ ) => {{ + $crate::current_ref!( $( $editor ).+ ).1 + }}; +} + +#[macro_export] +macro_rules! current_ref { + ( $( $editor:ident ).+ ) => {{ + let view = $( $editor ).+ .tree.get($( $editor ).+ .tree.focus); + let doc = &$( $editor ).+ .documents[view.doc]; + (view, doc) + }}; +} From 8ca91891d1f8f6df8073a4cc4af4132f8c6695ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Tue, 5 Oct 2021 22:35:38 +0900 Subject: [PATCH 4/8] fix compilation --- helix-term/src/application.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 79921268..8019b839 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -256,7 +256,7 @@ impl Application { // TODO: if completion window was closed with enter (and no selection) we shouldn't retrigger let mut cx = Context { - selected_register: helix_view::RegisterSelection::default(), + register: None, editor: &mut self.editor, jobs: &mut self.jobs, count: None, From f692ede2b7af9ee3c0a83642c0e169ac2db90ca5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Thu, 7 Oct 2021 10:37:35 +0900 Subject: [PATCH 5/8] fix: Don't crash on empty completion, don't retrigger on close --- helix-term/src/ui/editor.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index fda6a6d3..9234bb96 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -730,6 +730,12 @@ impl EditorView { ) { let mut completion = Completion::new(editor, items, offset_encoding, start_offset, trigger_offset); + + if completion.is_empty() { + // skip if we got no completion results + return; + } + // TODO : propagate required size on resize to completion too completion.required_size((size.width, size.height)); self.completion = Some(completion); @@ -939,6 +945,7 @@ impl Component for EditorView { if callback.is_some() { // assume close_fn self.completion = None; + cxt.editor.clear_idle_timer(); // don't retrigger } } } @@ -952,6 +959,7 @@ impl Component for EditorView { completion.update(&mut cxt); if completion.is_empty() { self.completion = None; + cxt.editor.clear_idle_timer(); // don't retrigger } } } From c7f3a971c0e42d1ecb8b91f075b2300040b656fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Sun, 10 Oct 2021 12:22:11 +0900 Subject: [PATCH 6/8] Remove resolved TODOs --- helix-term/src/application.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 8019b839..c39a9173 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -253,8 +253,6 @@ impl Application { return; } - // TODO: if completion window was closed with enter (and no selection) we shouldn't retrigger - let mut cx = Context { register: None, editor: &mut self.editor, @@ -264,7 +262,6 @@ impl Application { on_next_key_callback: None, }; completion(&mut cx); - // TODO: scan backwards for trigger and filter the box self.render(); } From 633b981db22bb30c601b838eaaeb814a64156125 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Sun, 10 Oct 2021 12:32:06 +0900 Subject: [PATCH 7/8] Make idle-timeout configurable --- book/src/configuration.md | 1 + helix-term/src/commands.rs | 11 +++++++++-- helix-view/src/editor.rs | 12 ++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/book/src/configuration.md b/book/src/configuration.md index 60b12bfd..f30146dd 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -19,6 +19,7 @@ To override global configuration parameters, create a `config.toml` file located | `line-number` | Line number display (`absolute`, `relative`) | `absolute` | | `smart-case` | Enable smart case regex searching (case insensitive unless pattern contains upper case characters) | `true` | | `auto-pairs` | Enable automatic insertion of pairs to parenthese, brackets, etc. | `true` | +| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. | `400` | ## LSP diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 3798c99a..f005e376 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -4130,7 +4130,7 @@ pub fn completion(cx: &mut Context) { }; if items.is_empty() { - editor.set_error("No completion available".to_string()); + // editor.set_error("No completion available".to_string()); return; } let size = compositor.size(); @@ -4138,7 +4138,14 @@ pub fn completion(cx: &mut Context) { .find(std::any::type_name::()) .unwrap(); if let Some(ui) = ui.as_any_mut().downcast_mut::() { - ui.set_completion(editor, items, offset_encoding, start_offset, trigger_offset, size); + ui.set_completion( + editor, + items, + offset_encoding, + start_offset, + trigger_offset, + size, + ); }; }, ); diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 5362acc8..5af6dbf3 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -26,6 +26,14 @@ use helix_core::Position; use serde::Deserialize; +fn deserialize_duration_millis<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let millis = u64::deserialize(deserializer)?; + Ok(Duration::from_millis(millis)) +} + #[derive(Debug, Clone, PartialEq, Deserialize)] #[serde(rename_all = "kebab-case", default)] pub struct Config { @@ -45,6 +53,9 @@ pub struct Config { pub smart_case: bool, /// Automatic insertion of pairs to parentheses, brackets, etc. Defaults to true. pub auto_pairs: bool, + /// Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. Defaults to 400ms. + #[serde(skip_serializing, deserialize_with = "deserialize_duration_millis")] + pub idle_timeout: Duration, } #[derive(Debug, Clone, PartialEq, Eq, Deserialize)] @@ -72,6 +83,7 @@ impl Default for Config { middle_click_paste: true, smart_case: true, auto_pairs: true, + idle_timeout: Duration::from_millis(400), } } } From 76b1bbc23ad5fc47765472cd9e83727a43c97ff3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Sun, 10 Oct 2021 12:33:22 +0900 Subject: [PATCH 8/8] Allow trigger_offset to be unused for now --- helix-term/src/ui/completion.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 72b03274..ba009c50 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -70,6 +70,7 @@ impl menu::Item for CompletionItem { pub struct Completion { popup: Popup>, start_offset: usize, + #[allow(dead_code)] trigger_offset: usize, // TODO: maintain a completioncontext with trigger kind & trigger char }