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 2024-09-27 13:09:20 -07:00
parent cbac427383
commit d7eaf8b2a9
4 changed files with 289 additions and 173 deletions

View File

@ -375,6 +375,15 @@ impl<'a> Token<'a> {
is_terminated: true,
}
}
pub fn deep_clone(&self) -> Token<'static> {
Token {
kind: self.kind,
content_start: self.content_start,
content: Cow::Owned(self.content.to_string()),
is_terminated: self.is_terminated,
}
}
}
#[derive(Debug)]

View File

@ -15,7 +15,7 @@ use helix_view::document::{read_to_string, DEFAULT_LANGUAGE_NAME};
use helix_view::editor::{CloseError, ConfigEvent};
use helix_view::expansion;
use serde_json::Value;
use ui::completers::{self, Completer};
use ui::completers::{self, Completer, CompletionResult};
#[derive(Clone)]
pub struct TypableCommand {
@ -3711,7 +3711,7 @@ fn command_line_doc(input: &str) -> Option<Cow<str>> {
Some(Cow::Owned(doc))
}
fn complete_command_line(editor: &Editor, input: &str) -> Vec<ui::prompt::Completion> {
fn complete_command_line(editor: &Editor, input: &str) -> CompletionResult {
let (command, rest, complete_command) = command_line::split(input);
if complete_command {
@ -3726,7 +3726,7 @@ fn complete_command_line(editor: &Editor, input: &str) -> Vec<ui::prompt::Comple
} else {
TYPABLE_COMMAND_MAP
.get(command)
.map_or_else(Vec::new, |cmd| {
.map_or(CompletionResult::Immediate(Vec::new()), |cmd| {
let args_offset = command.len() + 1;
complete_command_args(editor, cmd, rest, args_offset)
})
@ -3738,7 +3738,7 @@ fn complete_command_args(
command: &TypableCommand,
input: &str,
offset: usize,
) -> Vec<ui::prompt::Completion> {
) -> CompletionResult {
use command_line::{CompletionState, ExpansionKind, Tokenizer};
// TODO: completion should depend on the location of the cursor instead of the end of the
@ -3778,7 +3778,7 @@ fn complete_command_args(
// Don't complete on closed tokens, for example after writing a closing double quote.
if token.is_terminated {
return Vec::new();
return CompletionResult::Immediate(Vec::new());
}
match token.kind {
@ -3793,10 +3793,15 @@ fn complete_command_args(
.expect("completion state to be positional");
let completer = command.completer_for_argument_number(n);
completer(editor, &token.content)
.into_iter()
.map(|(range, span)| quote_completion(&token, range, span, offset))
.collect()
completer(editor, &token.content).map({
let token = token.deep_clone();
move |completions| {
completions
.into_iter()
.map(|(range, span)| quote_completion(&token, range, span, offset))
.collect()
}
})
}
CompletionState::Flag(_) => fuzzy_match(
token.content.trim_start_matches('-'),
@ -3832,7 +3837,7 @@ fn complete_command_args(
TokenKind::Expansion(ExpansionKind::Variable) => {
complete_variable_expansion(&token.content, offset + token.content_start)
}
TokenKind::Expansion(ExpansionKind::Unicode) => Vec::new(),
TokenKind::Expansion(ExpansionKind::Unicode) => CompletionResult::Immediate(Vec::new()),
TokenKind::ExpansionKind => {
complete_expansion_kind(&token.content, offset + token.content_start)
}
@ -3890,7 +3895,7 @@ fn complete_expand(
token: &Token,
completer: Option<&Completer>,
offset: usize,
) -> Vec<ui::prompt::Completion> {
) -> CompletionResult {
use command_line::{ExpansionKind, Tokenizer};
let mut start = 0;
@ -3935,15 +3940,20 @@ fn complete_expand(
match completer {
// If no expansions were found and an argument is being completed,
Some(completer) if start == 0 => completer(editor, &token.content)
.into_iter()
.map(|(range, span)| quote_completion(token, range, span, offset))
.collect(),
_ => Vec::new(),
Some(completer) if start == 0 => completer(editor, &token.content).map({
let token = token.deep_clone();
move |completions| {
completions
.into_iter()
.map(|(range, span)| quote_completion(&token, range, span, offset))
.collect()
}
}),
_ => CompletionResult::Immediate(Vec::new()),
}
}
fn complete_variable_expansion(content: &str, offset: usize) -> Vec<ui::prompt::Completion> {
fn complete_variable_expansion(content: &str, offset: usize) -> CompletionResult {
use expansion::Variable;
fuzzy_match(
@ -3956,7 +3966,7 @@ fn complete_variable_expansion(content: &str, offset: usize) -> Vec<ui::prompt::
.collect()
}
fn complete_expansion_kind(content: &str, offset: usize) -> Vec<ui::prompt::Completion> {
fn complete_expansion_kind(content: &str, offset: usize) -> CompletionResult {
use command_line::ExpansionKind;
fuzzy_match(

View File

@ -17,6 +17,7 @@ mod text_decorations;
use crate::compositor::Compositor;
use crate::filter_picker_entry;
use crate::job::{self, Callback};
use crate::ui::completers::CompletionResult;
pub use completion::Completion;
pub use editor::EditorView;
use helix_stdx::rope;
@ -50,7 +51,7 @@ pub fn prompt(
cx: &mut crate::commands::Context,
prompt: std::borrow::Cow<'static, str>,
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,
) {
let mut prompt = Prompt::new(prompt, history_register, completion_fn, callback_fn);
@ -59,24 +60,11 @@ pub fn 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(
cx: &mut crate::commands::Context,
prompt: std::borrow::Cow<'static, str>,
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,
) {
raw_regex_prompt(
@ -91,7 +79,7 @@ pub fn raw_regex_prompt(
cx: &mut crate::commands::Context,
prompt: std::borrow::Cow<'static, str>,
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,
) {
let (view, doc) = current!(cx.editor);
@ -382,13 +370,40 @@ pub mod completers {
use std::collections::BTreeSet;
use tui::text::Span;
pub type Completer = fn(&Editor, &str) -> Vec<Completion>;
pub fn none(_editor: &Editor, _input: &str) -> Vec<Completion> {
Vec::new()
pub enum CompletionResult {
Immediate(Vec<Completion>),
Callback(Box<dyn FnOnce() -> Vec<Completion> + Send + Sync>),
}
pub fn buffer(editor: &Editor, input: &str) -> Vec<Completion> {
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())),
}
}
}
impl FromIterator<Completion> for CompletionResult {
fn from_iter<T: IntoIterator<Item = Completion>>(items: T) -> Self {
Self::Immediate(items.into_iter().collect())
}
}
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| {
doc.relative_path()
.map(|p| p.display().to_string().into())
@ -401,20 +416,23 @@ pub mod completers {
.collect()
}
pub fn theme(_editor: &Editor, input: &str) -> Vec<Completion> {
let mut names = theme::Loader::read_names(&helix_loader::config_dir().join("themes"));
for rt_dir in helix_loader::runtime_dirs() {
names.extend(theme::Loader::read_names(&rt_dir.join("themes")));
}
names.push("default".into());
names.push("base16_default".into());
names.sort();
names.dedup();
pub fn theme(_editor: &Editor, input: &str) -> CompletionResult {
let input = String::from(input);
callback(move || {
let mut names = theme::Loader::read_names(&helix_loader::config_dir().join("themes"));
for rt_dir in helix_loader::runtime_dirs() {
names.extend(theme::Loader::read_names(&rt_dir.join("themes")));
}
names.push("default".into());
names.push("base16_default".into());
names.sort();
names.dedup();
fuzzy_match(input, names, false)
.into_iter()
.map(|(name, _)| ((0..), name.into()))
.collect()
fuzzy_match(&input, names, false)
.into_iter()
.map(|(name, _)| ((0..), name.into()))
.collect()
})
}
/// Recursive function to get all keys from this value and add them to vec
@ -434,7 +452,7 @@ pub mod completers {
}
/// Completes names of language servers which are running for the current document.
pub fn active_language_servers(editor: &Editor, input: &str) -> Vec<Completion> {
pub fn active_language_servers(editor: &Editor, input: &str) -> CompletionResult {
let language_servers = doc!(editor).language_servers().map(|ls| ls.name());
fuzzy_match(input, language_servers, false)
@ -445,7 +463,7 @@ pub mod completers {
/// Completes names of language servers which are configured for the language of the current
/// document.
pub fn configured_language_servers(editor: &Editor, input: &str) -> Vec<Completion> {
pub fn configured_language_servers(editor: &Editor, input: &str) -> CompletionResult {
let language_servers = doc!(editor)
.language_config()
.into_iter()
@ -458,7 +476,7 @@ pub mod completers {
.collect()
}
pub fn setting(_editor: &Editor, input: &str) -> Vec<Completion> {
pub fn setting(_editor: &Editor, input: &str) -> CompletionResult {
static KEYS: Lazy<Vec<String>> = Lazy::new(|| {
let mut keys = Vec::new();
let json = serde_json::json!(Config::default());
@ -472,7 +490,7 @@ pub mod completers {
.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)
}
@ -480,7 +498,7 @@ pub mod completers {
editor: &Editor,
input: &str,
git_ignore: bool,
) -> Vec<Completion> {
) -> CompletionResult {
filename_impl(editor, input, git_ignore, |entry| {
let is_dir = entry.file_type().is_some_and(|entry| entry.is_dir());
@ -492,7 +510,7 @@ pub mod completers {
})
}
pub fn language(editor: &Editor, input: &str) -> Vec<Completion> {
pub fn language(editor: &Editor, input: &str) -> CompletionResult {
let text: String = "text".into();
let loader = editor.syn_loader.load();
@ -507,7 +525,7 @@ pub mod completers {
.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)
.language_servers_with_feature(LanguageServerFeature::WorkspaceCommand)
.flat_map(|ls| {
@ -523,7 +541,7 @@ pub mod completers {
.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)
}
@ -531,7 +549,7 @@ pub mod completers {
editor: &Editor,
input: &str,
git_ignore: bool,
) -> Vec<Completion> {
) -> CompletionResult {
filename_impl(editor, input, git_ignore, |entry| {
let is_dir = entry.file_type().is_some_and(|entry| entry.is_dir());
@ -560,113 +578,117 @@ pub mod completers {
input: &str,
git_ignore: bool,
filter_fn: F,
) -> Vec<Completion>
) -> CompletionResult
where
F: Fn(&ignore::DirEntry) -> FileMatch,
F: Fn(&ignore::DirEntry) -> FileMatch + Send + Sync + 'static,
{
// Rust's filename handling is really annoying.
use ignore::WalkBuilder;
use std::path::Path;
let is_tilde = input == "~";
let path = helix_stdx::path::expand_tilde(Path::new(input));
let input = String::from(input);
let directory_color = editor.theme.get("ui.text.directory");
let (dir, file_name) = if input.ends_with(std::path::MAIN_SEPARATOR) {
(path, None)
} else {
let is_period = (input.ends_with((format!("{}.", std::path::MAIN_SEPARATOR)).as_str())
&& input.len() > 2)
|| input == ".";
let file_name = if is_period {
Some(String::from("."))
callback(move || {
let is_tilde = input == "~";
let path = helix_stdx::path::expand_tilde(Path::new(&input));
let (dir, file_name) = if input.ends_with(std::path::MAIN_SEPARATOR) {
(path, None)
} else {
path.file_name()
.and_then(|file| file.to_str().map(|path| path.to_owned()))
let is_period = (input
.ends_with((format!("{}.", std::path::MAIN_SEPARATOR)).as_str())
&& input.len() > 2)
|| input == ".";
let file_name = if is_period {
Some(String::from("."))
} else {
path.file_name()
.and_then(|file| file.to_str().map(|path| path.to_owned()))
};
let path = if is_period {
path
} else {
match path.parent() {
Some(path) if !path.as_os_str().is_empty() => Cow::Borrowed(path),
// Path::new("h")'s parent is Some("")...
_ => Cow::Owned(helix_stdx::env::current_working_dir()),
}
};
(path, file_name)
};
let path = if is_period {
path
} else {
match path.parent() {
Some(path) if !path.as_os_str().is_empty() => Cow::Borrowed(path),
// Path::new("h")'s parent is Some("")...
_ => Cow::Owned(helix_stdx::env::current_working_dir()),
let end = input.len()..;
let files = WalkBuilder::new(&dir)
.hidden(false)
.follow_links(false) // We're scanning over depth 1
.git_ignore(git_ignore)
.max_depth(Some(1))
.build()
.filter_map(|file| {
file.ok().and_then(|entry| {
let fmatch = filter_fn(&entry);
if fmatch == FileMatch::Reject {
return None;
}
let is_dir = entry.file_type().is_some_and(|entry| entry.is_dir());
let path = entry.path();
let mut path = if is_tilde {
// if it's a single tilde an absolute path is displayed so that when `TAB` is pressed on
// one of the directories the tilde will be replaced with a valid path not with a relative
// home directory name.
// ~ -> <TAB> -> /home/user
// ~/ -> <TAB> -> ~/first_entry
path.to_path_buf()
} else {
path.strip_prefix(&dir).unwrap_or(path).to_path_buf()
};
if fmatch == FileMatch::AcceptIncomplete {
path.push("");
}
let path = path.into_os_string().into_string().ok()?;
Some(Utf8PathBuf { path, is_dir })
})
}) // TODO: unwrap or skip
.filter(|path| !path.path.is_empty());
let style_from_file = |file: Utf8PathBuf| {
if file.is_dir {
Span::styled(file.path, directory_color)
} else {
Span::raw(file.path)
}
};
(path, file_name)
};
// if empty, return a list of dirs and files in current dir
if let Some(file_name) = file_name {
let range = (input.len().saturating_sub(file_name.len()))..;
fuzzy_match(&file_name, files, true)
.into_iter()
.map(|(name, _)| (range.clone(), style_from_file(name)))
.collect()
let end = input.len()..;
let files = WalkBuilder::new(&dir)
.hidden(false)
.follow_links(false) // We're scanning over depth 1
.git_ignore(git_ignore)
.max_depth(Some(1))
.build()
.filter_map(|file| {
file.ok().and_then(|entry| {
let fmatch = filter_fn(&entry);
if fmatch == FileMatch::Reject {
return None;
}
let is_dir = entry.file_type().is_some_and(|entry| entry.is_dir());
let path = entry.path();
let mut path = if is_tilde {
// if it's a single tilde an absolute path is displayed so that when `TAB` is pressed on
// one of the directories the tilde will be replaced with a valid path not with a relative
// home directory name.
// ~ -> <TAB> -> /home/user
// ~/ -> <TAB> -> ~/first_entry
path.to_path_buf()
} else {
path.strip_prefix(&dir).unwrap_or(path).to_path_buf()
};
if fmatch == FileMatch::AcceptIncomplete {
path.push("");
}
let path = path.into_os_string().into_string().ok()?;
Some(Utf8PathBuf { path, is_dir })
})
}) // TODO: unwrap or skip
.filter(|path| !path.path.is_empty());
let directory_color = editor.theme.get("ui.text.directory");
let style_from_file = |file: Utf8PathBuf| {
if file.is_dir {
Span::styled(file.path, directory_color)
// TODO: complete to longest common match
} else {
Span::raw(file.path)
let mut files: Vec<_> = files
.map(|file| (end.clone(), style_from_file(file)))
.collect();
files.sort_unstable_by(|(_, path1), (_, path2)| path1.content.cmp(&path2.content));
files
}
};
// if empty, return a list of dirs and files in current dir
if let Some(file_name) = file_name {
let range = (input.len().saturating_sub(file_name.len()))..;
fuzzy_match(&file_name, files, true)
.into_iter()
.map(|(name, _)| (range.clone(), style_from_file(name)))
.collect()
// TODO: complete to longest common match
} else {
let mut files: Vec<_> = files
.map(|file| (end.clone(), style_from_file(file)))
.collect();
files.sort_unstable_by(|(_, path1), (_, path2)| path1.content.cmp(&path2.content));
files
}
})
}
pub fn register(editor: &Editor, input: &str) -> Vec<Completion> {
pub fn register(editor: &Editor, input: &str) -> CompletionResult {
let iter = editor
.registers
.iter_preview()
@ -680,7 +702,7 @@ pub mod completers {
.collect()
}
pub fn program(_editor: &Editor, input: &str) -> Vec<Completion> {
pub fn program(_editor: &Editor, input: &str) -> CompletionResult {
static PROGRAMS_IN_PATH: Lazy<BTreeSet<String>> = Lazy::new(|| {
// Go through the entire PATH and read all files into a set.
let Some(path) = std::env::var_os("PATH") else {
@ -708,7 +730,7 @@ pub mod completers {
}
/// This expects input to be a raw string of arguments, because this is what Signature's raw_after does.
pub fn repeating_filenames(editor: &Editor, input: &str) -> Vec<Completion> {
pub fn repeating_filenames(editor: &Editor, input: &str) -> CompletionResult {
let token = match Tokenizer::new(input, false).last() {
Some(token) => token.unwrap(),
None => return filename(editor, input),
@ -716,26 +738,28 @@ pub mod completers {
let offset = token.content_start;
let mut completions = filename(editor, &input[offset..]);
for completion in completions.iter_mut() {
completion.0.start += offset;
}
completions
filename(editor, &input[offset..]).map(move |mut completions| {
for completion in completions.iter_mut() {
completion.0.start += offset;
}
completions
})
}
pub fn shell(editor: &Editor, input: &str) -> Vec<Completion> {
pub fn shell(editor: &Editor, input: &str) -> CompletionResult {
let (command, args, complete_command) = command_line::split(input);
if complete_command {
return program(editor, command);
}
let mut completions = repeating_filenames(editor, args);
for completion in completions.iter_mut() {
// + 1 for separator between `command` and `args`
completion.0.start += command.len() + 1;
}
completions
let len = command.len();
repeating_filenames(editor, args).map(move |mut completions| {
for completion in completions.iter_mut() {
// + 1 for separator between `command` and `args`
completion.0.start += len + 1;
}
completions
})
}
}

View File

@ -1,12 +1,16 @@
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 helix_core::syntax;
use helix_event::{AsyncHook, TaskController, TaskHandle};
use helix_view::document::Mode;
use helix_view::input::KeyEvent;
use helix_view::keyboard::KeyCode;
use std::sync::Arc;
use std::time::Duration;
use std::{borrow::Cow, ops::RangeFrom};
use tokio::time::Instant;
use tui::buffer::Buffer as Surface;
use tui::text::Span;
use tui::widgets::{Block, Widget};
@ -19,10 +23,39 @@ use helix_view::{
Editor,
};
type PromptCharHandler = Box<dyn Fn(&mut Prompt, char, &Context)>;
pub type Completion = (RangeFrom<usize>, Span<'static>);
type CompletionFn = Box<dyn FnMut(&Editor, &str) -> Vec<Completion>>;
struct CompletionEvent {
cancel: TaskHandle,
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, event: CompletionEvent, _: Option<Instant>) -> Option<Instant> {
if event.cancel.is_canceled() {
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)>;
pub type DocFn = Box<dyn Fn(&str) -> Option<Cow<str>>>;
@ -36,11 +69,14 @@ pub struct Prompt {
truncate_start: bool,
truncate_end: bool,
// ---
completion_fn: CompletionFn,
completion_hook: tokio::sync::mpsc::Sender<CompletionEvent>,
task_controller: TaskController,
receive_completion: Option<std::sync::mpsc::Receiver<Vec<Completion>>>,
completion: Vec<Completion>,
selection: Option<usize>,
history_register: Option<char>,
history_pos: Option<usize>,
completion_fn: CompletionFn,
callback_fn: CallbackFn,
pub doc_fn: DocFn,
next_char_handler: Option<PromptCharHandler>,
@ -81,7 +117,7 @@ impl Prompt {
pub fn new(
prompt: Cow<'static, str>,
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,
) -> Self {
Self {
@ -92,11 +128,14 @@ impl Prompt {
anchor: 0,
truncate_start: false,
truncate_end: false,
completion_fn: Box::new(completion_fn),
completion_hook: CompletionHandler {}.spawn(),
task_controller: TaskController::new(),
receive_completion: None,
completion: Vec::new(),
selection: None,
history_register,
history_pos: None,
completion_fn: Box::new(completion_fn),
callback_fn: Box::new(callback_fn),
doc_fn: Box::new(|_| None),
next_char_handler: None,
@ -153,8 +192,33 @@ impl Prompt {
}
pub fn recalculate_completion(&mut self, editor: &Editor) {
// Cancel any pending async completions.
let handle = self.task_controller.restart();
self.receive_completion = None;
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 (send_completion, recv_completion) = std::sync::mpsc::sync_channel(1);
helix_event::send_blocking(
&self.completion_hook,
CompletionEvent {
cancel: handle,
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(50)) {
self.completion = completion;
return;
}
self.completion.clear();
self.receive_completion = Some(recv_completion);
}
}
}
/// Compute the cursor position after applying movement
@ -394,6 +458,16 @@ impl Prompt {
pub fn exit_selection(&mut self) {
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;
@ -406,7 +480,6 @@ impl Prompt {
let selected_color = theme.get("ui.menu.selected");
let suggestion_color = theme.get("ui.text.inactive");
let background = theme.get("ui.background");
// completion
let max_len = self
.completion