Handle prompt completions with AsyncHook.

This change allows prompt completions to be calculated in the
background, to avoid blocking the UI on slow file IO such as over a
networked FS.
pull/11787/head
Rose Hogenson 2 months ago
parent 30aa375f2d
commit 08bef881cc

@ -12,7 +12,7 @@ use helix_core::{line_ending, shellwords::Shellwords};
use helix_view::document::{read_to_string, DEFAULT_LANGUAGE_NAME}; use helix_view::document::{read_to_string, DEFAULT_LANGUAGE_NAME};
use helix_view::editor::{CloseError, ConfigEvent}; use helix_view::editor::{CloseError, ConfigEvent};
use serde_json::Value; use serde_json::Value;
use ui::completers::{self, Completer}; use ui::completers::{self, Completer, CompletionResult};
#[derive(Clone)] #[derive(Clone)]
pub struct TypableCommand { pub struct TypableCommand {
@ -3187,19 +3187,22 @@ pub(super) fn command_mode(cx: &mut Context) {
.get(&words[0] as &str) .get(&words[0] as &str)
.map(|tc| tc.completer_for_argument_number(argument_number)) .map(|tc| tc.completer_for_argument_number(argument_number))
{ {
completer(editor, word) let input = String::from(input);
.into_iter() completer(editor, word).map(move |completion| {
.map(|(range, file)| { completion
let file = shellwords::escape(file); .into_iter()
.map(|(range, file)| {
// offset ranges to input let file = shellwords::escape(file);
let offset = input.len() - word_len;
let range = (range.start + offset)..; // offset ranges to input
(range, file) let offset = input.len() - word_len;
}) let range = (range.start + offset)..;
.collect() (range, file)
})
.collect()
})
} else { } else {
Vec::new() CompletionResult::Immediate(Vec::new())
} }
} }
}, // completion }, // completion

@ -17,6 +17,7 @@ mod text_decorations;
use crate::compositor::Compositor; use crate::compositor::Compositor;
use crate::filter_picker_entry; use crate::filter_picker_entry;
use crate::job::{self, Callback}; use crate::job::{self, Callback};
use crate::ui::completers::CompletionResult;
pub use completion::{Completion, CompletionItem}; pub use completion::{Completion, CompletionItem};
pub use editor::EditorView; pub use editor::EditorView;
use helix_stdx::rope; use helix_stdx::rope;
@ -36,7 +37,7 @@ pub fn prompt(
cx: &mut crate::commands::Context, cx: &mut crate::commands::Context,
prompt: std::borrow::Cow<'static, str>, prompt: std::borrow::Cow<'static, str>,
history_register: Option<char>, history_register: Option<char>,
completion_fn: impl FnMut(&Editor, &str) -> Vec<prompt::Completion> + 'static, completion_fn: impl FnMut(&Editor, &str) -> CompletionResult + 'static,
callback_fn: impl FnMut(&mut crate::compositor::Context, &str, PromptEvent) + 'static, callback_fn: impl FnMut(&mut crate::compositor::Context, &str, PromptEvent) + 'static,
) { ) {
let mut prompt = Prompt::new(prompt, history_register, completion_fn, callback_fn); let mut prompt = Prompt::new(prompt, history_register, completion_fn, callback_fn);
@ -45,24 +46,11 @@ pub fn prompt(
cx.push_layer(Box::new(prompt)); cx.push_layer(Box::new(prompt));
} }
pub fn prompt_with_input(
cx: &mut crate::commands::Context,
prompt: std::borrow::Cow<'static, str>,
input: String,
history_register: Option<char>,
completion_fn: impl FnMut(&Editor, &str) -> Vec<prompt::Completion> + 'static,
callback_fn: impl FnMut(&mut crate::compositor::Context, &str, PromptEvent) + 'static,
) {
let prompt = Prompt::new(prompt, history_register, completion_fn, callback_fn)
.with_line(input, cx.editor);
cx.push_layer(Box::new(prompt));
}
pub fn regex_prompt( pub fn regex_prompt(
cx: &mut crate::commands::Context, cx: &mut crate::commands::Context,
prompt: std::borrow::Cow<'static, str>, prompt: std::borrow::Cow<'static, str>,
history_register: Option<char>, history_register: Option<char>,
completion_fn: impl FnMut(&Editor, &str) -> Vec<prompt::Completion> + 'static, completion_fn: impl FnMut(&Editor, &str) -> CompletionResult + 'static,
fun: impl Fn(&mut crate::compositor::Context, rope::Regex, PromptEvent) + 'static, fun: impl Fn(&mut crate::compositor::Context, rope::Regex, PromptEvent) + 'static,
) { ) {
raw_regex_prompt( raw_regex_prompt(
@ -77,7 +65,7 @@ pub fn raw_regex_prompt(
cx: &mut crate::commands::Context, cx: &mut crate::commands::Context,
prompt: std::borrow::Cow<'static, str>, prompt: std::borrow::Cow<'static, str>,
history_register: Option<char>, history_register: Option<char>,
completion_fn: impl FnMut(&Editor, &str) -> Vec<prompt::Completion> + 'static, completion_fn: impl FnMut(&Editor, &str) -> CompletionResult + 'static,
fun: impl Fn(&mut crate::compositor::Context, rope::Regex, &str, PromptEvent) + 'static, fun: impl Fn(&mut crate::compositor::Context, rope::Regex, &str, PromptEvent) + 'static,
) { ) {
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
@ -275,13 +263,40 @@ pub mod completers {
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::borrow::Cow; use std::borrow::Cow;
pub type Completer = fn(&Editor, &str) -> Vec<Completion>; pub enum CompletionResult {
Immediate(Vec<Completion>),
Callback(Box<dyn FnOnce() -> Vec<Completion> + Send + Sync>),
}
fn callback(f: impl FnOnce() -> Vec<Completion> + Send + Sync + 'static) -> CompletionResult {
CompletionResult::Callback(Box::new(f))
}
impl CompletionResult {
pub fn map(
self,
f: impl FnOnce(Vec<Completion>) -> Vec<Completion> + Send + Sync + 'static,
) -> CompletionResult {
match self {
CompletionResult::Immediate(v) => CompletionResult::Immediate(f(v)),
CompletionResult::Callback(v) => callback(move || f(v())),
}
}
}
pub fn none(_editor: &Editor, _input: &str) -> Vec<Completion> { impl FromIterator<Completion> for CompletionResult {
Vec::new() fn from_iter<T: IntoIterator<Item = Completion>>(items: T) -> Self {
Self::Immediate(items.into_iter().collect())
}
} }
pub fn buffer(editor: &Editor, input: &str) -> Vec<Completion> { pub type Completer = fn(&Editor, &str) -> CompletionResult;
pub fn none(_editor: &Editor, _input: &str) -> CompletionResult {
CompletionResult::Immediate(Vec::new())
}
pub fn buffer(editor: &Editor, input: &str) -> CompletionResult {
let names = editor.documents.values().map(|doc| { let names = editor.documents.values().map(|doc| {
doc.relative_path() doc.relative_path()
.map(|p| p.display().to_string().into()) .map(|p| p.display().to_string().into())
@ -294,20 +309,23 @@ pub mod completers {
.collect() .collect()
} }
pub fn theme(_editor: &Editor, input: &str) -> Vec<Completion> { pub fn theme(_editor: &Editor, input: &str) -> CompletionResult {
let mut names = theme::Loader::read_names(&helix_loader::config_dir().join("themes")); let input = String::from(input);
for rt_dir in helix_loader::runtime_dirs() { callback(move || {
names.extend(theme::Loader::read_names(&rt_dir.join("themes"))); let mut names = theme::Loader::read_names(&helix_loader::config_dir().join("themes"));
} for rt_dir in helix_loader::runtime_dirs() {
names.push("default".into()); names.extend(theme::Loader::read_names(&rt_dir.join("themes")));
names.push("base16_default".into()); }
names.sort(); names.push("default".into());
names.dedup(); names.push("base16_default".into());
names.sort();
names.dedup();
fuzzy_match(input, names, false) fuzzy_match(&input, names, false)
.into_iter() .into_iter()
.map(|(name, _)| ((0..), name.into())) .map(|(name, _)| ((0..), name.into()))
.collect() .collect()
})
} }
/// Recursive function to get all keys from this value and add them to vec /// Recursive function to get all keys from this value and add them to vec
@ -326,7 +344,7 @@ pub mod completers {
} }
} }
pub fn setting(_editor: &Editor, input: &str) -> Vec<Completion> { pub fn setting(_editor: &Editor, input: &str) -> CompletionResult {
static KEYS: Lazy<Vec<String>> = Lazy::new(|| { static KEYS: Lazy<Vec<String>> = Lazy::new(|| {
let mut keys = Vec::new(); let mut keys = Vec::new();
let json = serde_json::json!(Config::default()); let json = serde_json::json!(Config::default());
@ -340,27 +358,30 @@ pub mod completers {
.collect() .collect()
} }
pub fn filename(editor: &Editor, input: &str) -> Vec<Completion> { pub fn filename(editor: &Editor, input: &str) -> CompletionResult {
filename_with_git_ignore(editor, input, true) filename_with_git_ignore(editor, input, true)
} }
pub fn filename_with_git_ignore( pub fn filename_with_git_ignore(
editor: &Editor, _editor: &Editor,
input: &str, input: &str,
git_ignore: bool, git_ignore: bool,
) -> Vec<Completion> { ) -> CompletionResult {
filename_impl(editor, input, git_ignore, |entry| { let input = String::from(input);
let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir()); callback(move || {
filename_impl(&input, git_ignore, |entry| {
if is_dir { let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir());
FileMatch::AcceptIncomplete
} else { if is_dir {
FileMatch::Accept FileMatch::AcceptIncomplete
} } else {
FileMatch::Accept
}
})
}) })
} }
pub fn language(editor: &Editor, input: &str) -> Vec<Completion> { pub fn language(editor: &Editor, input: &str) -> CompletionResult {
let text: String = "text".into(); let text: String = "text".into();
let loader = editor.syn_loader.load(); let loader = editor.syn_loader.load();
@ -375,7 +396,7 @@ pub mod completers {
.collect() .collect()
} }
pub fn lsp_workspace_command(editor: &Editor, input: &str) -> Vec<Completion> { pub fn lsp_workspace_command(editor: &Editor, input: &str) -> CompletionResult {
let commands = doc!(editor) let commands = doc!(editor)
.language_servers_with_feature(LanguageServerFeature::WorkspaceCommand) .language_servers_with_feature(LanguageServerFeature::WorkspaceCommand)
.flat_map(|ls| { .flat_map(|ls| {
@ -391,23 +412,26 @@ pub mod completers {
.collect() .collect()
} }
pub fn directory(editor: &Editor, input: &str) -> Vec<Completion> { pub fn directory(editor: &Editor, input: &str) -> CompletionResult {
directory_with_git_ignore(editor, input, true) directory_with_git_ignore(editor, input, true)
} }
pub fn directory_with_git_ignore( pub fn directory_with_git_ignore(
editor: &Editor, _editor: &Editor,
input: &str, input: &str,
git_ignore: bool, git_ignore: bool,
) -> Vec<Completion> { ) -> CompletionResult {
filename_impl(editor, input, git_ignore, |entry| { let input = String::from(input);
let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir()); callback(move || {
filename_impl(&input, git_ignore, |entry| {
if is_dir { let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir());
FileMatch::Accept
} else { if is_dir {
FileMatch::Reject FileMatch::Accept
} } else {
FileMatch::Reject
}
})
}) })
} }
@ -423,12 +447,7 @@ pub mod completers {
} }
// TODO: we could return an iter/lazy thing so it can fetch as many as it needs. // TODO: we could return an iter/lazy thing so it can fetch as many as it needs.
fn filename_impl<F>( fn filename_impl<F>(input: &str, git_ignore: bool, filter_fn: F) -> Vec<Completion>
_editor: &Editor,
input: &str,
git_ignore: bool,
filter_fn: F,
) -> Vec<Completion>
where where
F: Fn(&ignore::DirEntry) -> FileMatch, F: Fn(&ignore::DirEntry) -> FileMatch,
{ {
@ -522,7 +541,7 @@ pub mod completers {
} }
} }
pub fn register(editor: &Editor, input: &str) -> Vec<Completion> { pub fn register(editor: &Editor, input: &str) -> CompletionResult {
let iter = editor let iter = editor
.registers .registers
.iter_preview() .iter_preview()

@ -1,12 +1,16 @@
use crate::compositor::{Component, Compositor, Context, Event, EventResult}; use crate::compositor::{Component, Compositor, Context, Event, EventResult};
use crate::{alt, ctrl, key, shift, ui}; use crate::ui::completers::CompletionResult;
use crate::{alt, ctrl, job, key, shift, ui};
use arc_swap::ArcSwap; use arc_swap::ArcSwap;
use helix_core::syntax; use helix_core::syntax;
use helix_event::{AsyncHook, CancelRx, CancelTx};
use helix_view::document::Mode; use helix_view::document::Mode;
use helix_view::input::KeyEvent; use helix_view::input::KeyEvent;
use helix_view::keyboard::KeyCode; use helix_view::keyboard::KeyCode;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration;
use std::{borrow::Cow, ops::RangeFrom}; use std::{borrow::Cow, ops::RangeFrom};
use tokio::time::Instant;
use tui::buffer::Buffer as Surface; use tui::buffer::Buffer as Surface;
use tui::widgets::{Block, Widget}; use tui::widgets::{Block, Widget};
@ -18,9 +22,39 @@ use helix_view::{
Editor, Editor,
}; };
type PromptCharHandler = Box<dyn Fn(&mut Prompt, char, &Context)>;
pub type Completion = (RangeFrom<usize>, Cow<'static, str>); pub type Completion = (RangeFrom<usize>, Cow<'static, str>);
type CompletionFn = Box<dyn FnMut(&Editor, &str) -> Vec<Completion>>; struct CompletionEvent {
cancel: CancelRx,
callback: Box<dyn FnOnce() -> Vec<Completion> + Send + Sync>,
send: std::sync::mpsc::SyncSender<Vec<Completion>>,
}
struct CompletionHandler {}
impl helix_event::AsyncHook for CompletionHandler {
type Event = CompletionEvent;
fn handle_event(&mut self, mut event: CompletionEvent, _: Option<Instant>) -> Option<Instant> {
let Err(tokio::sync::oneshot::error::TryRecvError::Empty) = event.cancel.try_recv() else {
return None;
};
let completion = (event.callback)();
if event.send.send(completion).is_err() {
return None;
}
job::dispatch_blocking(move |_editor, compositor| {
if let Some(prompt) = compositor.find::<Prompt>() {
prompt.process_async_completion();
}
});
None
}
fn finish_debounce(&mut self) {}
}
type PromptCharHandler = Box<dyn Fn(&mut Prompt, char, &Context)>;
type CompletionFn = Box<dyn FnMut(&Editor, &str) -> CompletionResult>;
type CallbackFn = Box<dyn FnMut(&mut Context, &str, PromptEvent)>; type CallbackFn = Box<dyn FnMut(&mut Context, &str, PromptEvent)>;
pub type DocFn = Box<dyn Fn(&str) -> Option<Cow<str>>>; pub type DocFn = Box<dyn Fn(&str) -> Option<Cow<str>>>;
@ -28,11 +62,14 @@ pub struct Prompt {
prompt: Cow<'static, str>, prompt: Cow<'static, str>,
line: String, line: String,
cursor: usize, cursor: usize,
completion_fn: CompletionFn,
completion_hook: tokio::sync::mpsc::Sender<CompletionEvent>,
completion_ctx: Option<CancelTx>,
receive_completion: Option<std::sync::mpsc::Receiver<Vec<Completion>>>,
completion: Vec<Completion>, completion: Vec<Completion>,
selection: Option<usize>, selection: Option<usize>,
history_register: Option<char>, history_register: Option<char>,
history_pos: Option<usize>, history_pos: Option<usize>,
completion_fn: CompletionFn,
callback_fn: CallbackFn, callback_fn: CallbackFn,
pub doc_fn: DocFn, pub doc_fn: DocFn,
next_char_handler: Option<PromptCharHandler>, next_char_handler: Option<PromptCharHandler>,
@ -73,18 +110,21 @@ impl Prompt {
pub fn new( pub fn new(
prompt: Cow<'static, str>, prompt: Cow<'static, str>,
history_register: Option<char>, history_register: Option<char>,
completion_fn: impl FnMut(&Editor, &str) -> Vec<Completion> + 'static, completion_fn: impl FnMut(&Editor, &str) -> CompletionResult + 'static,
callback_fn: impl FnMut(&mut Context, &str, PromptEvent) + 'static, callback_fn: impl FnMut(&mut Context, &str, PromptEvent) + 'static,
) -> Self { ) -> Self {
Self { Self {
prompt, prompt,
line: String::new(), line: String::new(),
cursor: 0, cursor: 0,
completion_fn: Box::new(completion_fn),
completion_hook: CompletionHandler {}.spawn(),
completion_ctx: None,
receive_completion: None,
completion: Vec::new(), completion: Vec::new(),
selection: None, selection: None,
history_register, history_register,
history_pos: None, history_pos: None,
completion_fn: Box::new(completion_fn),
callback_fn: Box::new(callback_fn), callback_fn: Box::new(callback_fn),
doc_fn: Box::new(|_| None), doc_fn: Box::new(|_| None),
next_char_handler: None, next_char_handler: None,
@ -141,8 +181,35 @@ impl Prompt {
} }
pub fn recalculate_completion(&mut self, editor: &Editor) { pub fn recalculate_completion(&mut self, editor: &Editor) {
// Cancel any pending async completions.
self.completion_ctx = None;
self.receive_completion = None;
self.exit_selection(); self.exit_selection();
self.completion = (self.completion_fn)(editor, &self.line); match (self.completion_fn)(editor, &self.line) {
CompletionResult::Immediate(completion) => self.completion = completion,
CompletionResult::Callback(f) => {
let (cancel_tx, cancel_rx) = helix_event::cancelation();
let (send_completion, recv_completion) = std::sync::mpsc::sync_channel(1);
helix_event::send_blocking(
&self.completion_hook,
CompletionEvent {
cancel: cancel_rx,
callback: f,
send: send_completion,
},
);
// To avoid flicker, give the completion handler a small timeout to
// complete immediately.
if let Ok(completion) = recv_completion.recv_timeout(Duration::from_millis(100)) {
self.completion = completion;
return;
}
self.completion.clear();
self.completion_ctx = Some(cancel_tx);
self.receive_completion = Some(recv_completion);
}
}
} }
/// Compute the cursor position after applying movement /// Compute the cursor position after applying movement
@ -390,6 +457,16 @@ impl Prompt {
pub fn exit_selection(&mut self) { pub fn exit_selection(&mut self) {
self.selection = None; self.selection = None;
} }
fn process_async_completion(&mut self) {
let Some(receive_completion) = &self.receive_completion else {
return;
};
if let Ok(completion) = receive_completion.try_recv() {
self.completion = completion;
helix_event::request_redraw();
}
}
} }
const BASE_WIDTH: u16 = 30; const BASE_WIDTH: u16 = 30;
@ -402,7 +479,6 @@ impl Prompt {
let selected_color = theme.get("ui.menu.selected"); let selected_color = theme.get("ui.menu.selected");
let suggestion_color = theme.get("ui.text.inactive"); let suggestion_color = theme.get("ui.text.inactive");
let background = theme.get("ui.background"); let background = theme.get("ui.background");
// completion
let max_len = self let max_len = self
.completion .completion

Loading…
Cancel
Save