mirror of https://github.com/helix-editor/helix
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
parent
cbac427383
commit
d7eaf8b2a9
|
@ -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)]
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue