aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBlaž Hrastnik2021-10-10 13:11:01 +0000
committerGitHub2021-10-10 13:11:01 +0000
commitf8f63c55081ee966c7cb2b84139c25c6301b5fff (patch)
treec4c74c5c013d8c26f6b9b124821fabd9200c3218
parenta7f49fa56fecd7f44efca7e6074e5cd9e5d91c92 (diff)
parent76b1bbc23ad5fc47765472cd9e83727a43c97ff3 (diff)
Merge pull request #821 from helix-editor/idle-timer
Idle timer / Autocompletion
-rw-r--r--book/src/configuration.md1
-rw-r--r--helix-term/src/application.rs37
-rw-r--r--helix-term/src/commands.rs28
-rw-r--r--helix-term/src/ui/completion.rs26
-rw-r--r--helix-term/src/ui/editor.rs16
-rw-r--r--helix-view/src/editor.rs32
-rw-r--r--helix-view/src/macros.rs16
7 files changed, 140 insertions, 16 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/application.rs b/helix-term/src/application.rs
index 6206e6f2..c39a9173 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 => {
+ // idle timeout
+ self.editor.clear_idle_timer();
+ self.handle_idle_timeout();
+ }
}
}
}
@@ -228,6 +233,38 @@ impl Application {
}
}
+ pub fn handle_idle_timeout(&mut self) {
+ use crate::commands::{completion, Context};
+ use helix_view::document::Mode;
+
+ if doc_mut!(self.editor).mode != Mode::Insert {
+ return;
+ }
+ let editor_view = self
+ .compositor
+ .find(std::any::type_name::<ui::EditorView>())
+ .expect("expected at least one EditorView");
+ let editor_view = editor_view
+ .as_any_mut()
+ .downcast_mut::<ui::EditorView>()
+ .unwrap();
+
+ if editor_view.completion.is_some() {
+ return;
+ }
+
+ let mut cx = Context {
+ register: None,
+ editor: &mut self.editor,
+ jobs: &mut self.jobs,
+ count: None,
+ callback: None,
+ on_next_key_callback: None,
+ };
+ completion(&mut cx);
+ self.render();
+ }
+
pub fn handle_terminal_events(&mut self, event: Option<Result<Event, crossterm::ErrorKind>>) {
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 c1fd7bfe..95c46a4e 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -4051,7 +4051,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
@@ -4096,10 +4096,8 @@ 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);
@@ -4107,6 +4105,15 @@ 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,
@@ -4129,7 +4136,7 @@ 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();
@@ -4137,7 +4144,14 @@ fn completion(cx: &mut Context) {
.find(std::any::type_name::<ui::EditorView>())
.unwrap();
if let Some(ui) = ui.as_any_mut().downcast_mut::<ui::EditorView>() {
- 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..ba009c50 100644
--- a/helix-term/src/ui/completion.rs
+++ b/helix-term/src/ui/completion.rs
@@ -69,14 +69,18 @@ impl menu::Item for CompletionItem {
/// Wraps a Menu.
pub struct Completion {
popup: Popup<Menu<CompletionItem>>,
+ start_offset: usize,
+ #[allow(dead_code)]
trigger_offset: usize,
// TODO: maintain a completioncontext with trigger kind & trigger char
}
impl Completion {
pub fn new(
+ editor: &Editor,
items: Vec<CompletionItem>,
offset_encoding: helix_lsp::OffsetEncoding,
+ start_offset: usize,
trigger_offset: usize,
) -> Self {
// let items: Vec<CompletionItem> = Vec::new();
@@ -175,16 +179,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 +210,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 0605e2c7..9234bb96 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<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>,
last_insert: (commands::Command, Vec<KeyEvent>),
- completion: Option<Completion>,
+ pub(crate) completion: Option<Completion>,
spinners: ProgressSpinners,
autoinfo: Option<Info>,
}
@@ -721,12 +721,21 @@ impl EditorView {
pub fn set_completion(
&mut self,
+ editor: &Editor,
items: Vec<helix_lsp::lsp::CompletionItem>,
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);
+
+ 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);
@@ -901,6 +910,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
@@ -935,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
}
}
}
@@ -948,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
}
}
}
diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs
index b08a2df2..5af6dbf3 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;
@@ -24,6 +26,14 @@ use helix_core::Position;
use serde::Deserialize;
+fn deserialize_duration_millis<'de, D>(deserializer: D) -> Result<Duration, D::Error>
+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 {
@@ -43,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)]
@@ -70,6 +83,7 @@ impl Default for Config {
middle_click_paste: true,
smart_case: true,
auto_pairs: true,
+ idle_timeout: Duration::from_millis(400),
}
}
}
@@ -91,6 +105,8 @@ pub struct Editor {
pub status_msg: Option<(String, Severity)>,
pub config: Config,
+
+ pub idle_timer: Pin<Box<Sleep>>,
}
#[derive(Debug, Copy, Clone)]
@@ -125,10 +141,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;
}
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)
+ }};
+}