mirror of
https://github.com/helix-editor/helix.git
synced 2024-11-23 01:46:18 +04:00
Merge pull request #821 from helix-editor/idle-timer
Idle timer / Autocompletion
This commit is contained in:
commit
f8f63c5508
@ -19,6 +19,7 @@ ## Editor
|
|||||||
| `line-number` | Line number display (`absolute`, `relative`) | `absolute` |
|
| `line-number` | Line number display (`absolute`, `relative`) | `absolute` |
|
||||||
| `smart-case` | Enable smart case regex searching (case insensitive unless pattern contains upper case characters) | `true` |
|
| `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` |
|
| `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
|
## LSP
|
||||||
|
|
||||||
|
@ -199,6 +199,11 @@ pub async fn event_loop(&mut self) {
|
|||||||
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
|
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
|
||||||
self.render();
|
self.render();
|
||||||
}
|
}
|
||||||
|
_ = &mut self.editor.idle_timer => {
|
||||||
|
// idle timeout
|
||||||
|
self.editor.clear_idle_timer();
|
||||||
|
self.handle_idle_timeout();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -228,6 +233,38 @@ pub async fn handle_signals(&mut self, signal: i32) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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>>) {
|
pub fn handle_terminal_events(&mut self, event: Option<Result<Event, crossterm::ErrorKind>>) {
|
||||||
let mut cx = crate::compositor::Context {
|
let mut cx = crate::compositor::Context {
|
||||||
editor: &mut self.editor,
|
editor: &mut self.editor,
|
||||||
|
@ -4051,7 +4051,7 @@ fn remove_primary_selection(cx: &mut Context) {
|
|||||||
doc.set_selection(view.id, selection);
|
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
|
// trigger on trigger char, or if user calls it
|
||||||
// (or on word char typing??)
|
// (or on word char typing??)
|
||||||
// after it's triggered, if response marked is_incomplete, update on every subsequent keypress
|
// 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 offset_encoding = language_server.offset_encoding();
|
||||||
let cursor = doc
|
let text = doc.text().slice(..);
|
||||||
.selection(view.id)
|
let cursor = doc.selection(view.id).primary().cursor(text);
|
||||||
.primary()
|
|
||||||
.cursor(doc.text().slice(..));
|
|
||||||
|
|
||||||
let pos = pos_to_lsp_pos(doc.text(), cursor, offset_encoding);
|
let pos = pos_to_lsp_pos(doc.text(), cursor, offset_encoding);
|
||||||
|
|
||||||
@ -4107,6 +4105,15 @@ fn completion(cx: &mut Context) {
|
|||||||
|
|
||||||
let trigger_offset = cursor;
|
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(
|
cx.callback(
|
||||||
future,
|
future,
|
||||||
move |editor: &mut Editor,
|
move |editor: &mut Editor,
|
||||||
@ -4129,7 +4136,7 @@ fn completion(cx: &mut Context) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if items.is_empty() {
|
if items.is_empty() {
|
||||||
editor.set_error("No completion available".to_string());
|
// editor.set_error("No completion available".to_string());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let size = compositor.size();
|
let size = compositor.size();
|
||||||
@ -4137,7 +4144,14 @@ fn completion(cx: &mut Context) {
|
|||||||
.find(std::any::type_name::<ui::EditorView>())
|
.find(std::any::type_name::<ui::EditorView>())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
if let Some(ui) = ui.as_any_mut().downcast_mut::<ui::EditorView>() {
|
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,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -69,14 +69,18 @@ fn row(&self) -> menu::Row {
|
|||||||
/// Wraps a Menu.
|
/// Wraps a Menu.
|
||||||
pub struct Completion {
|
pub struct Completion {
|
||||||
popup: Popup<Menu<CompletionItem>>,
|
popup: Popup<Menu<CompletionItem>>,
|
||||||
|
start_offset: usize,
|
||||||
|
#[allow(dead_code)]
|
||||||
trigger_offset: usize,
|
trigger_offset: usize,
|
||||||
// TODO: maintain a completioncontext with trigger kind & trigger char
|
// TODO: maintain a completioncontext with trigger kind & trigger char
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Completion {
|
impl Completion {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
|
editor: &Editor,
|
||||||
items: Vec<CompletionItem>,
|
items: Vec<CompletionItem>,
|
||||||
offset_encoding: helix_lsp::OffsetEncoding,
|
offset_encoding: helix_lsp::OffsetEncoding,
|
||||||
|
start_offset: usize,
|
||||||
trigger_offset: usize,
|
trigger_offset: usize,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
// let items: Vec<CompletionItem> = Vec::new();
|
// let items: Vec<CompletionItem> = Vec::new();
|
||||||
@ -175,16 +179,22 @@ fn item_to_transaction(
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
let popup = Popup::new(menu);
|
let popup = Popup::new(menu);
|
||||||
Self {
|
let mut completion = Self {
|
||||||
popup,
|
popup,
|
||||||
|
start_offset,
|
||||||
trigger_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
|
// recompute menu based on matches
|
||||||
let menu = self.popup.contents_mut();
|
let menu = self.popup.contents_mut();
|
||||||
let (view, doc) = current!(cx.editor);
|
let (view, doc) = current_ref!(editor);
|
||||||
|
|
||||||
// cx.hooks()
|
// cx.hooks()
|
||||||
// cx.add_hook(enum type, ||)
|
// cx.add_hook(enum type, ||)
|
||||||
@ -200,14 +210,18 @@ pub fn update(&mut self, cx: &mut commands::Context) {
|
|||||||
.selection(view.id)
|
.selection(view.id)
|
||||||
.primary()
|
.primary()
|
||||||
.cursor(doc.text().slice(..));
|
.cursor(doc.text().slice(..));
|
||||||
if self.trigger_offset <= cursor {
|
if self.start_offset <= cursor {
|
||||||
let fragment = doc.text().slice(self.trigger_offset..cursor);
|
let fragment = doc.text().slice(self.start_offset..cursor);
|
||||||
let text = Cow::from(fragment);
|
let text = Cow::from(fragment);
|
||||||
// TODO: logic is same as ui/picker
|
// TODO: logic is same as ui/picker
|
||||||
menu.score(&text);
|
menu.score(&text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, cx: &mut commands::Context) {
|
||||||
|
self.recompute_filter(cx.editor)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.popup.contents().is_empty()
|
self.popup.contents().is_empty()
|
||||||
}
|
}
|
||||||
|
@ -33,7 +33,7 @@ pub struct EditorView {
|
|||||||
keymaps: Keymaps,
|
keymaps: Keymaps,
|
||||||
on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>,
|
on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>,
|
||||||
last_insert: (commands::Command, Vec<KeyEvent>),
|
last_insert: (commands::Command, Vec<KeyEvent>),
|
||||||
completion: Option<Completion>,
|
pub(crate) completion: Option<Completion>,
|
||||||
spinners: ProgressSpinners,
|
spinners: ProgressSpinners,
|
||||||
autoinfo: Option<Info>,
|
autoinfo: Option<Info>,
|
||||||
}
|
}
|
||||||
@ -721,12 +721,21 @@ fn command_mode(&mut self, mode: Mode, cxt: &mut commands::Context, event: KeyEv
|
|||||||
|
|
||||||
pub fn set_completion(
|
pub fn set_completion(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
editor: &Editor,
|
||||||
items: Vec<helix_lsp::lsp::CompletionItem>,
|
items: Vec<helix_lsp::lsp::CompletionItem>,
|
||||||
offset_encoding: helix_lsp::OffsetEncoding,
|
offset_encoding: helix_lsp::OffsetEncoding,
|
||||||
|
start_offset: usize,
|
||||||
trigger_offset: usize,
|
trigger_offset: usize,
|
||||||
size: Rect,
|
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
|
// TODO : propagate required size on resize to completion too
|
||||||
completion.required_size((size.width, size.height));
|
completion.required_size((size.width, size.height));
|
||||||
self.completion = Some(completion);
|
self.completion = Some(completion);
|
||||||
@ -901,6 +910,7 @@ fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
|
|||||||
EventResult::Consumed(None)
|
EventResult::Consumed(None)
|
||||||
}
|
}
|
||||||
Event::Key(key) => {
|
Event::Key(key) => {
|
||||||
|
cxt.editor.reset_idle_timer();
|
||||||
let mut key = KeyEvent::from(key);
|
let mut key = KeyEvent::from(key);
|
||||||
canonicalize_key(&mut key);
|
canonicalize_key(&mut key);
|
||||||
// clear status
|
// clear status
|
||||||
@ -935,6 +945,7 @@ fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
|
|||||||
if callback.is_some() {
|
if callback.is_some() {
|
||||||
// assume close_fn
|
// assume close_fn
|
||||||
self.completion = None;
|
self.completion = None;
|
||||||
|
cxt.editor.clear_idle_timer(); // don't retrigger
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -948,6 +959,7 @@ fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
|
|||||||
completion.update(&mut cxt);
|
completion.update(&mut cxt);
|
||||||
if completion.is_empty() {
|
if completion.is_empty() {
|
||||||
self.completion = None;
|
self.completion = None;
|
||||||
|
cxt.editor.clear_idle_timer(); // don't retrigger
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,10 +9,12 @@
|
|||||||
use futures_util::future;
|
use futures_util::future;
|
||||||
use std::{
|
use std::{
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
|
pin::Pin,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
time::Duration,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use tokio::time::{sleep, Duration, Instant, Sleep};
|
||||||
|
|
||||||
use slotmap::SlotMap;
|
use slotmap::SlotMap;
|
||||||
|
|
||||||
use anyhow::Error;
|
use anyhow::Error;
|
||||||
@ -24,6 +26,14 @@
|
|||||||
|
|
||||||
use serde::Deserialize;
|
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)]
|
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||||
#[serde(rename_all = "kebab-case", default)]
|
#[serde(rename_all = "kebab-case", default)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
@ -43,6 +53,9 @@ pub struct Config {
|
|||||||
pub smart_case: bool,
|
pub smart_case: bool,
|
||||||
/// Automatic insertion of pairs to parentheses, brackets, etc. Defaults to true.
|
/// Automatic insertion of pairs to parentheses, brackets, etc. Defaults to true.
|
||||||
pub auto_pairs: bool,
|
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)]
|
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||||
@ -70,6 +83,7 @@ fn default() -> Self {
|
|||||||
middle_click_paste: true,
|
middle_click_paste: true,
|
||||||
smart_case: true,
|
smart_case: true,
|
||||||
auto_pairs: true,
|
auto_pairs: true,
|
||||||
|
idle_timeout: Duration::from_millis(400),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -91,6 +105,8 @@ pub struct Editor {
|
|||||||
pub status_msg: Option<(String, Severity)>,
|
pub status_msg: Option<(String, Severity)>,
|
||||||
|
|
||||||
pub config: Config,
|
pub config: Config,
|
||||||
|
|
||||||
|
pub idle_timer: Pin<Box<Sleep>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
@ -125,10 +141,24 @@ pub fn new(
|
|||||||
registers: Registers::default(),
|
registers: Registers::default(),
|
||||||
clipboard_provider: get_clipboard_provider(),
|
clipboard_provider: get_clipboard_provider(),
|
||||||
status_msg: None,
|
status_msg: None,
|
||||||
|
idle_timer: Box::pin(sleep(Duration::from_millis(500))),
|
||||||
config,
|
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) {
|
pub fn clear_status(&mut self) {
|
||||||
self.status_msg = None;
|
self.status_msg = None;
|
||||||
}
|
}
|
||||||
|
@ -44,3 +44,19 @@ macro_rules! view {
|
|||||||
$( $editor ).+ .tree.get($( $editor ).+ .tree.focus)
|
$( $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)
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user