on master help sign in
Merge pull request #821 from helix-editor/idle-timer
f8f63c5Blaž Hrastnik
book/src/configuration.md+1
@@ -19,6 +19,7 @@ To override global configuration parameters, create a `config.toml` file located
19| `line-number` | Line number display (`absolute`, `relative`) | `absolute` |
19| `line-number` | Line number display (`absolute`, `relative`) | `absolute` |
20| `smart-case` | Enable smart case regex searching (case insensitive unless pattern contains upper case characters) | `true` |
20| `smart-case` | Enable smart case regex searching (case insensitive unless pattern contains upper case characters) | `true` |
21| `auto-pairs` | Enable automatic insertion of pairs to parenthese, brackets, etc. | `true` |
21| `auto-pairs` | Enable automatic insertion of pairs to parenthese, brackets, etc. | `true` |
22+| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. | `400` |
22
23
23## LSP
24## LSP
24
25
helix-term/src/application.rs+37
@@ -199,6 +199,11 @@ impl Application {
199 self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
199 self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
200 self.render();
200 self.render();
201 }
201 }
202+ _ = &mut self.editor.idle_timer => {
203+ // idle timeout
204+ self.editor.clear_idle_timer();
205+ self.handle_idle_timeout();
206+ }
202 }
207 }
203 }
208 }
204 }
209 }
@@ -228,6 +233,38 @@ impl Application {
228 }
233 }
229 }
234 }
230
235
236+ pub fn handle_idle_timeout(&mut self) {
237+ use crate::commands::{completion, Context};
238+ use helix_view::document::Mode;
239+
240+ if doc_mut!(self.editor).mode != Mode::Insert {
241+ return;
242+ }
243+ let editor_view = self
244+ .compositor
245+ .find(std::any::type_name::<ui::EditorView>())
246+ .expect("expected at least one EditorView");
247+ let editor_view = editor_view
248+ .as_any_mut()
249+ .downcast_mut::<ui::EditorView>()
250+ .unwrap();
251+
252+ if editor_view.completion.is_some() {
253+ return;
254+ }
255+
256+ let mut cx = Context {
257+ register: None,
258+ editor: &mut self.editor,
259+ jobs: &mut self.jobs,
260+ count: None,
261+ callback: None,
262+ on_next_key_callback: None,
263+ };
264+ completion(&mut cx);
265+ self.render();
266+ }
267+
231 pub fn handle_terminal_events(&mut self, event: Option<Result<Event, crossterm::ErrorKind>>) {
268 pub fn handle_terminal_events(&mut self, event: Option<Result<Event, crossterm::ErrorKind>>) {
232 let mut cx = crate::compositor::Context {
269 let mut cx = crate::compositor::Context {
233 editor: &mut self.editor,
270 editor: &mut self.editor,
helix-term/src/commands.rs+21−7
@@ -4051,7 +4051,7 @@ fn remove_primary_selection(cx: &mut Context) {
4051 doc.set_selection(view.id, selection);
4051 doc.set_selection(view.id, selection);
4052}
4052}
4053
4053
4054−fn completion(cx: &mut Context) {
4054+pub fn completion(cx: &mut Context) {
4055 // trigger on trigger char, or if user calls it
4055 // trigger on trigger char, or if user calls it
4056 // (or on word char typing??)
4056 // (or on word char typing??)
4057 // after it's triggered, if response marked is_incomplete, update on every subsequent keypress
4057 // after it's triggered, if response marked is_incomplete, update on every subsequent keypress
@@ -4096,10 +4096,8 @@ fn completion(cx: &mut Context) {
4096 };
4096 };
4097
4097
4098 let offset_encoding = language_server.offset_encoding();
4098 let offset_encoding = language_server.offset_encoding();
4099− let cursor = doc
4099+ let text = doc.text().slice(..);
4100− .selection(view.id)
4100+ let cursor = doc.selection(view.id).primary().cursor(text);
4101− .primary()
4102− .cursor(doc.text().slice(..));
4103
4101
4104 let pos = pos_to_lsp_pos(doc.text(), cursor, offset_encoding);
4102 let pos = pos_to_lsp_pos(doc.text(), cursor, offset_encoding);
4105
4103
@@ -4107,6 +4105,15 @@ fn completion(cx: &mut Context) {
4107
4105
4108 let trigger_offset = cursor;
4106 let trigger_offset = cursor;
4109
4107
4108+ // TODO: trigger_offset should be the cursor offset but we also need a starting offset from where we want to apply
4109+ // completion filtering. For example logger.te| should filter the initial suggestion list with "te".
4110+
4111+ use helix_core::chars;
4112+ let mut iter = text.chars_at(cursor);
4113+ iter.reverse();
4114+ let offset = iter.take_while(|ch| chars::char_is_word(*ch)).count();
4115+ let start_offset = cursor.saturating_sub(offset);
4116+
4110 cx.callback(
4117 cx.callback(
4111 future,
4118 future,
4112 move |editor: &mut Editor,
4119 move |editor: &mut Editor,
@@ -4129,7 +4136,7 @@ fn completion(cx: &mut Context) {
4129 };
4136 };
4130
4137
4131 if items.is_empty() {
4138 if items.is_empty() {
4132− editor.set_error("No completion available".to_string());
4139+ // editor.set_error("No completion available".to_string());
4133 return;
4140 return;
4134 }
4141 }
4135 let size = compositor.size();
4142 let size = compositor.size();
@@ -4137,7 +4144,14 @@ fn completion(cx: &mut Context) {
4137 .find(std::any::type_name::<ui::EditorView>())
4144 .find(std::any::type_name::<ui::EditorView>())
4138 .unwrap();
4145 .unwrap();
4139 if let Some(ui) = ui.as_any_mut().downcast_mut::<ui::EditorView>() {
4146 if let Some(ui) = ui.as_any_mut().downcast_mut::<ui::EditorView>() {
4140− ui.set_completion(items, offset_encoding, trigger_offset, size);
4147+ ui.set_completion(
4148+ editor,
4149+ items,
4150+ offset_encoding,
4151+ start_offset,
4152+ trigger_offset,
4153+ size,
4154+ );
4141 };
4155 };
4142 },
4156 },
4143 );
4157 );
helix-term/src/ui/completion.rs+20−6
@@ -69,14 +69,18 @@ impl menu::Item for CompletionItem {
69/// Wraps a Menu.
69/// Wraps a Menu.
70pub struct Completion {
70pub struct Completion {
71 popup: Popup<Menu<CompletionItem>>,
71 popup: Popup<Menu<CompletionItem>>,
72+ start_offset: usize,
73+ #[allow(dead_code)]
72 trigger_offset: usize,
74 trigger_offset: usize,
73 // TODO: maintain a completioncontext with trigger kind & trigger char
75 // TODO: maintain a completioncontext with trigger kind & trigger char
74}
76}
75
77
76impl Completion {
78impl Completion {
77 pub fn new(
79 pub fn new(
80+ editor: &Editor,
78 items: Vec<CompletionItem>,
81 items: Vec<CompletionItem>,
79 offset_encoding: helix_lsp::OffsetEncoding,
82 offset_encoding: helix_lsp::OffsetEncoding,
83+ start_offset: usize,
80 trigger_offset: usize,
84 trigger_offset: usize,
81 ) -> Self {
85 ) -> Self {
82 // let items: Vec<CompletionItem> = Vec::new();
86 // let items: Vec<CompletionItem> = Vec::new();
@@ -175,16 +179,22 @@ impl Completion {
175 };
179 };
176 });
180 });
177 let popup = Popup::new(menu);
181 let popup = Popup::new(menu);
178− Self {
182+ let mut completion = Self {
179 popup,
183 popup,
184+ start_offset,
180 trigger_offset,
185 trigger_offset,
181− }
186+ };
187+
188+ // need to recompute immediately in case start_offset != trigger_offset
189+ completion.recompute_filter(editor);
190+
191+ completion
182 }
192 }
183
193
184− pub fn update(&mut self, cx: &mut commands::Context) {
194+ pub fn recompute_filter(&mut self, editor: &Editor) {
185 // recompute menu based on matches
195 // recompute menu based on matches
186 let menu = self.popup.contents_mut();
196 let menu = self.popup.contents_mut();
187− let (view, doc) = current!(cx.editor);
197+ let (view, doc) = current_ref!(editor);
188
198
189 // cx.hooks()
199 // cx.hooks()
190 // cx.add_hook(enum type, ||)
200 // cx.add_hook(enum type, ||)
@@ -200,14 +210,18 @@ impl Completion {
200 .selection(view.id)
210 .selection(view.id)
201 .primary()
211 .primary()
202 .cursor(doc.text().slice(..));
212 .cursor(doc.text().slice(..));
203− if self.trigger_offset <= cursor {
213+ if self.start_offset <= cursor {
204− let fragment = doc.text().slice(self.trigger_offset..cursor);
214+ let fragment = doc.text().slice(self.start_offset..cursor);
205 let text = Cow::from(fragment);
215 let text = Cow::from(fragment);
206 // TODO: logic is same as ui/picker
216 // TODO: logic is same as ui/picker
207 menu.score(&text);
217 menu.score(&text);
208 }
218 }
209 }
219 }
210
220
221+ pub fn update(&mut self, cx: &mut commands::Context) {
222+ self.recompute_filter(cx.editor)
223+ }
224+
211 pub fn is_empty(&self) -> bool {
225 pub fn is_empty(&self) -> bool {
212 self.popup.contents().is_empty()
226 self.popup.contents().is_empty()
213 }
227 }
helix-term/src/ui/editor.rs+14−2
@@ -33,7 +33,7 @@ pub struct EditorView {
33 keymaps: Keymaps,
33 keymaps: Keymaps,
34 on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>,
34 on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>,
35 last_insert: (commands::Command, Vec<KeyEvent>),
35 last_insert: (commands::Command, Vec<KeyEvent>),
36− completion: Option<Completion>,
36+ pub(crate) completion: Option<Completion>,
37 spinners: ProgressSpinners,
37 spinners: ProgressSpinners,
38 autoinfo: Option<Info>,
38 autoinfo: Option<Info>,
39}
39}
@@ -721,12 +721,21 @@ impl EditorView {
721
721
722 pub fn set_completion(
722 pub fn set_completion(
723 &mut self,
723 &mut self,
724+ editor: &Editor,
724 items: Vec<helix_lsp::lsp::CompletionItem>,
725 items: Vec<helix_lsp::lsp::CompletionItem>,
725 offset_encoding: helix_lsp::OffsetEncoding,
726 offset_encoding: helix_lsp::OffsetEncoding,
727+ start_offset: usize,
726 trigger_offset: usize,
728 trigger_offset: usize,
727 size: Rect,
729 size: Rect,
728 ) {
730 ) {
731+ let mut completion =
729− let mut completion = Completion::new(items, offset_encoding, trigger_offset);
732+ Completion::new(editor, items, offset_encoding, start_offset, trigger_offset);
733+
734+ if completion.is_empty() {
735+ // skip if we got no completion results
736+ return;
737+ }
738+
730 // TODO : propagate required size on resize to completion too
739 // TODO : propagate required size on resize to completion too
731 completion.required_size((size.width, size.height));
740 completion.required_size((size.width, size.height));
732 self.completion = Some(completion);
741 self.completion = Some(completion);
@@ -901,6 +910,7 @@ impl Component for EditorView {
901 EventResult::Consumed(None)
910 EventResult::Consumed(None)
902 }
911 }
903 Event::Key(key) => {
912 Event::Key(key) => {
913+ cxt.editor.reset_idle_timer();
904 let mut key = KeyEvent::from(key);
914 let mut key = KeyEvent::from(key);
905 canonicalize_key(&mut key);
915 canonicalize_key(&mut key);
906 // clear status
916 // clear status
@@ -935,6 +945,7 @@ impl Component for EditorView {
935 if callback.is_some() {
945 if callback.is_some() {
936 // assume close_fn
946 // assume close_fn
937 self.completion = None;
947 self.completion = None;
948+ cxt.editor.clear_idle_timer(); // don't retrigger
938 }
949 }
939 }
950 }
940 }
951 }
@@ -948,6 +959,7 @@ impl Component for EditorView {
948 completion.update(&mut cxt);
959 completion.update(&mut cxt);
949 if completion.is_empty() {
960 if completion.is_empty() {
950 self.completion = None;
961 self.completion = None;
962+ cxt.editor.clear_idle_timer(); // don't retrigger
951 }
963 }
952 }
964 }
953 }
965 }
helix-view/src/editor.rs+31−1
@@ -9,10 +9,12 @@ use crate::{
9use futures_util::future;
9use futures_util::future;
10use std::{
10use std::{
11 path::{Path, PathBuf},
11 path::{Path, PathBuf},
12+ pin::Pin,
12 sync::Arc,
13 sync::Arc,
13− time::Duration,
14};
14};
15
15
16+use tokio::time::{sleep, Duration, Instant, Sleep};
17+
16use slotmap::SlotMap;
18use slotmap::SlotMap;
17
19
18use anyhow::Error;
20use anyhow::Error;
@@ -24,6 +26,14 @@ use helix_core::Position;
24
26
25use serde::Deserialize;
27use serde::Deserialize;
26
28
29+fn deserialize_duration_millis<'de, D>(deserializer: D) -> Result<Duration, D::Error>
30+where
31+ D: serde::Deserializer<'de>,
32+{
33+ let millis = u64::deserialize(deserializer)?;
34+ Ok(Duration::from_millis(millis))
35+}
36+
27#[derive(Debug, Clone, PartialEq, Deserialize)]
37#[derive(Debug, Clone, PartialEq, Deserialize)]
28#[serde(rename_all = "kebab-case", default)]
38#[serde(rename_all = "kebab-case", default)]
29pub struct Config {
39pub struct Config {
@@ -43,6 +53,9 @@ pub struct Config {
43 pub smart_case: bool,
53 pub smart_case: bool,
44 /// Automatic insertion of pairs to parentheses, brackets, etc. Defaults to true.
54 /// Automatic insertion of pairs to parentheses, brackets, etc. Defaults to true.
45 pub auto_pairs: bool,
55 pub auto_pairs: bool,
56+ /// Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. Defaults to 400ms.
57+ #[serde(skip_serializing, deserialize_with = "deserialize_duration_millis")]
58+ pub idle_timeout: Duration,
46}
59}
47
60
48#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
61#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
@@ -70,6 +83,7 @@ impl Default for Config {
70 middle_click_paste: true,
83 middle_click_paste: true,
71 smart_case: true,
84 smart_case: true,
72 auto_pairs: true,
85 auto_pairs: true,
86+ idle_timeout: Duration::from_millis(400),
73 }
87 }
74 }
88 }
75}
89}
@@ -91,6 +105,8 @@ pub struct Editor {
91 pub status_msg: Option<(String, Severity)>,
105 pub status_msg: Option<(String, Severity)>,
92
106
93 pub config: Config,
107 pub config: Config,
108+
109+ pub idle_timer: Pin<Box<Sleep>>,
94}
110}
95
111
96#[derive(Debug, Copy, Clone)]
112#[derive(Debug, Copy, Clone)]
@@ -125,10 +141,24 @@ impl Editor {
125 registers: Registers::default(),
141 registers: Registers::default(),
126 clipboard_provider: get_clipboard_provider(),
142 clipboard_provider: get_clipboard_provider(),
127 status_msg: None,
143 status_msg: None,
144+ idle_timer: Box::pin(sleep(Duration::from_millis(500))),
128 config,
145 config,
129 }
146 }
130 }
147 }
131
148
149+ pub fn clear_idle_timer(&mut self) {
150+ // equivalent to internal Instant::far_future() (30 years)
151+ self.idle_timer
152+ .as_mut()
153+ .reset(Instant::now() + Duration::from_secs(86400 * 365 * 30));
154+ }
155+
156+ pub fn reset_idle_timer(&mut self) {
157+ self.idle_timer
158+ .as_mut()
159+ .reset(Instant::now() + Duration::from_millis(500));
160+ }
161+
132 pub fn clear_status(&mut self) {
162 pub fn clear_status(&mut self) {
133 self.status_msg = None;
163 self.status_msg = None;
134 }
164 }
helix-view/src/macros.rs+16
@@ -44,3 +44,19 @@ macro_rules! view {
44 $( $editor ).+ .tree.get($( $editor ).+ .tree.focus)
44 $( $editor ).+ .tree.get($( $editor ).+ .tree.focus)
45 }};
45 }};
46}
46}
47+
48+#[macro_export]
49+macro_rules! doc {
50+ ( $( $editor:ident ).+ ) => {{
51+ $crate::current_ref!( $( $editor ).+ ).1
52+ }};
53+}
54+
55+#[macro_export]
56+macro_rules! current_ref {
57+ ( $( $editor:ident ).+ ) => {{
58+ let view = $( $editor ).+ .tree.get($( $editor ).+ .tree.focus);
59+ let doc = &$( $editor ).+ .documents[view.doc];
60+ (view, doc)
61+ }};
62+}