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,
|
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)]
|
#[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::editor::{CloseError, ConfigEvent};
|
||||||
use helix_view::expansion;
|
use helix_view::expansion;
|
||||||
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 {
|
||||||
|
@ -3711,7 +3711,7 @@ fn command_line_doc(input: &str) -> Option<Cow<str>> {
|
||||||
Some(Cow::Owned(doc))
|
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);
|
let (command, rest, complete_command) = command_line::split(input);
|
||||||
|
|
||||||
if complete_command {
|
if complete_command {
|
||||||
|
@ -3726,7 +3726,7 @@ fn complete_command_line(editor: &Editor, input: &str) -> Vec<ui::prompt::Comple
|
||||||
} else {
|
} else {
|
||||||
TYPABLE_COMMAND_MAP
|
TYPABLE_COMMAND_MAP
|
||||||
.get(command)
|
.get(command)
|
||||||
.map_or_else(Vec::new, |cmd| {
|
.map_or(CompletionResult::Immediate(Vec::new()), |cmd| {
|
||||||
let args_offset = command.len() + 1;
|
let args_offset = command.len() + 1;
|
||||||
complete_command_args(editor, cmd, rest, args_offset)
|
complete_command_args(editor, cmd, rest, args_offset)
|
||||||
})
|
})
|
||||||
|
@ -3738,7 +3738,7 @@ fn complete_command_args(
|
||||||
command: &TypableCommand,
|
command: &TypableCommand,
|
||||||
input: &str,
|
input: &str,
|
||||||
offset: usize,
|
offset: usize,
|
||||||
) -> Vec<ui::prompt::Completion> {
|
) -> CompletionResult {
|
||||||
use command_line::{CompletionState, ExpansionKind, Tokenizer};
|
use command_line::{CompletionState, ExpansionKind, Tokenizer};
|
||||||
|
|
||||||
// TODO: completion should depend on the location of the cursor instead of the end of the
|
// 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.
|
// Don't complete on closed tokens, for example after writing a closing double quote.
|
||||||
if token.is_terminated {
|
if token.is_terminated {
|
||||||
return Vec::new();
|
return CompletionResult::Immediate(Vec::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
match token.kind {
|
match token.kind {
|
||||||
|
@ -3793,11 +3793,16 @@ fn complete_command_args(
|
||||||
.expect("completion state to be positional");
|
.expect("completion state to be positional");
|
||||||
let completer = command.completer_for_argument_number(n);
|
let completer = command.completer_for_argument_number(n);
|
||||||
|
|
||||||
completer(editor, &token.content)
|
completer(editor, &token.content).map({
|
||||||
|
let token = token.deep_clone();
|
||||||
|
move |completions| {
|
||||||
|
completions
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(range, span)| quote_completion(&token, range, span, offset))
|
.map(|(range, span)| quote_completion(&token, range, span, offset))
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
CompletionState::Flag(_) => fuzzy_match(
|
CompletionState::Flag(_) => fuzzy_match(
|
||||||
token.content.trim_start_matches('-'),
|
token.content.trim_start_matches('-'),
|
||||||
command.signature.flags.iter().map(|flag| flag.name),
|
command.signature.flags.iter().map(|flag| flag.name),
|
||||||
|
@ -3832,7 +3837,7 @@ fn complete_command_args(
|
||||||
TokenKind::Expansion(ExpansionKind::Variable) => {
|
TokenKind::Expansion(ExpansionKind::Variable) => {
|
||||||
complete_variable_expansion(&token.content, offset + token.content_start)
|
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 => {
|
TokenKind::ExpansionKind => {
|
||||||
complete_expansion_kind(&token.content, offset + token.content_start)
|
complete_expansion_kind(&token.content, offset + token.content_start)
|
||||||
}
|
}
|
||||||
|
@ -3890,7 +3895,7 @@ fn complete_expand(
|
||||||
token: &Token,
|
token: &Token,
|
||||||
completer: Option<&Completer>,
|
completer: Option<&Completer>,
|
||||||
offset: usize,
|
offset: usize,
|
||||||
) -> Vec<ui::prompt::Completion> {
|
) -> CompletionResult {
|
||||||
use command_line::{ExpansionKind, Tokenizer};
|
use command_line::{ExpansionKind, Tokenizer};
|
||||||
|
|
||||||
let mut start = 0;
|
let mut start = 0;
|
||||||
|
@ -3935,15 +3940,20 @@ fn complete_expand(
|
||||||
|
|
||||||
match completer {
|
match completer {
|
||||||
// If no expansions were found and an argument is being completed,
|
// If no expansions were found and an argument is being completed,
|
||||||
Some(completer) if start == 0 => completer(editor, &token.content)
|
Some(completer) if start == 0 => completer(editor, &token.content).map({
|
||||||
|
let token = token.deep_clone();
|
||||||
|
move |completions| {
|
||||||
|
completions
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(range, span)| quote_completion(token, range, span, offset))
|
.map(|(range, span)| quote_completion(&token, range, span, offset))
|
||||||
.collect(),
|
.collect()
|
||||||
_ => Vec::new(),
|
}
|
||||||
|
}),
|
||||||
|
_ => 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;
|
use expansion::Variable;
|
||||||
|
|
||||||
fuzzy_match(
|
fuzzy_match(
|
||||||
|
@ -3956,7 +3966,7 @@ fn complete_variable_expansion(content: &str, offset: usize) -> Vec<ui::prompt::
|
||||||
.collect()
|
.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;
|
use command_line::ExpansionKind;
|
||||||
|
|
||||||
fuzzy_match(
|
fuzzy_match(
|
||||||
|
|
|
@ -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;
|
pub use completion::Completion;
|
||||||
pub use editor::EditorView;
|
pub use editor::EditorView;
|
||||||
use helix_stdx::rope;
|
use helix_stdx::rope;
|
||||||
|
@ -50,7 +51,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);
|
||||||
|
@ -59,24 +60,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(
|
||||||
|
@ -91,7 +79,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);
|
||||||
|
@ -382,13 +370,40 @@ pub mod completers {
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
use tui::text::Span;
|
use tui::text::Span;
|
||||||
|
|
||||||
pub type Completer = fn(&Editor, &str) -> Vec<Completion>;
|
pub enum CompletionResult {
|
||||||
|
Immediate(Vec<Completion>),
|
||||||
pub fn none(_editor: &Editor, _input: &str) -> Vec<Completion> {
|
Callback(Box<dyn FnOnce() -> Vec<Completion> + Send + Sync>),
|
||||||
Vec::new()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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| {
|
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())
|
||||||
|
@ -401,7 +416,9 @@ pub mod completers {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn theme(_editor: &Editor, input: &str) -> Vec<Completion> {
|
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"));
|
let mut names = theme::Loader::read_names(&helix_loader::config_dir().join("themes"));
|
||||||
for rt_dir in helix_loader::runtime_dirs() {
|
for rt_dir in helix_loader::runtime_dirs() {
|
||||||
names.extend(theme::Loader::read_names(&rt_dir.join("themes")));
|
names.extend(theme::Loader::read_names(&rt_dir.join("themes")));
|
||||||
|
@ -411,10 +428,11 @@ pub mod completers {
|
||||||
names.sort();
|
names.sort();
|
||||||
names.dedup();
|
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
|
||||||
|
@ -434,7 +452,7 @@ pub mod completers {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Completes names of language servers which are running for the current document.
|
/// 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());
|
let language_servers = doc!(editor).language_servers().map(|ls| ls.name());
|
||||||
|
|
||||||
fuzzy_match(input, language_servers, false)
|
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
|
/// Completes names of language servers which are configured for the language of the current
|
||||||
/// document.
|
/// 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)
|
let language_servers = doc!(editor)
|
||||||
.language_config()
|
.language_config()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -458,7 +476,7 @@ pub mod completers {
|
||||||
.collect()
|
.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(|| {
|
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());
|
||||||
|
@ -472,7 +490,7 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -480,7 +498,7 @@ pub mod completers {
|
||||||
editor: &Editor,
|
editor: &Editor,
|
||||||
input: &str,
|
input: &str,
|
||||||
git_ignore: bool,
|
git_ignore: bool,
|
||||||
) -> Vec<Completion> {
|
) -> CompletionResult {
|
||||||
filename_impl(editor, input, git_ignore, |entry| {
|
filename_impl(editor, input, git_ignore, |entry| {
|
||||||
let is_dir = entry.file_type().is_some_and(|entry| entry.is_dir());
|
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 text: String = "text".into();
|
||||||
|
|
||||||
let loader = editor.syn_loader.load();
|
let loader = editor.syn_loader.load();
|
||||||
|
@ -507,7 +525,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| {
|
||||||
|
@ -523,7 +541,7 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -531,7 +549,7 @@ pub mod completers {
|
||||||
editor: &Editor,
|
editor: &Editor,
|
||||||
input: &str,
|
input: &str,
|
||||||
git_ignore: bool,
|
git_ignore: bool,
|
||||||
) -> Vec<Completion> {
|
) -> CompletionResult {
|
||||||
filename_impl(editor, input, git_ignore, |entry| {
|
filename_impl(editor, input, git_ignore, |entry| {
|
||||||
let is_dir = entry.file_type().is_some_and(|entry| entry.is_dir());
|
let is_dir = entry.file_type().is_some_and(|entry| entry.is_dir());
|
||||||
|
|
||||||
|
@ -560,22 +578,27 @@ pub mod completers {
|
||||||
input: &str,
|
input: &str,
|
||||||
git_ignore: bool,
|
git_ignore: bool,
|
||||||
filter_fn: F,
|
filter_fn: F,
|
||||||
) -> Vec<Completion>
|
) -> CompletionResult
|
||||||
where
|
where
|
||||||
F: Fn(&ignore::DirEntry) -> FileMatch,
|
F: Fn(&ignore::DirEntry) -> FileMatch + Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
// Rust's filename handling is really annoying.
|
// Rust's filename handling is really annoying.
|
||||||
|
|
||||||
use ignore::WalkBuilder;
|
use ignore::WalkBuilder;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
let input = String::from(input);
|
||||||
|
let directory_color = editor.theme.get("ui.text.directory");
|
||||||
|
|
||||||
|
callback(move || {
|
||||||
let is_tilde = input == "~";
|
let is_tilde = input == "~";
|
||||||
let path = helix_stdx::path::expand_tilde(Path::new(input));
|
let path = helix_stdx::path::expand_tilde(Path::new(&input));
|
||||||
|
|
||||||
let (dir, file_name) = if input.ends_with(std::path::MAIN_SEPARATOR) {
|
let (dir, file_name) = if input.ends_with(std::path::MAIN_SEPARATOR) {
|
||||||
(path, None)
|
(path, None)
|
||||||
} else {
|
} else {
|
||||||
let is_period = (input.ends_with((format!("{}.", std::path::MAIN_SEPARATOR)).as_str())
|
let is_period = (input
|
||||||
|
.ends_with((format!("{}.", std::path::MAIN_SEPARATOR)).as_str())
|
||||||
&& input.len() > 2)
|
&& input.len() > 2)
|
||||||
|| input == ".";
|
|| input == ".";
|
||||||
let file_name = if is_period {
|
let file_name = if is_period {
|
||||||
|
@ -638,8 +661,6 @@ pub mod completers {
|
||||||
}) // TODO: unwrap or skip
|
}) // TODO: unwrap or skip
|
||||||
.filter(|path| !path.path.is_empty());
|
.filter(|path| !path.path.is_empty());
|
||||||
|
|
||||||
let directory_color = editor.theme.get("ui.text.directory");
|
|
||||||
|
|
||||||
let style_from_file = |file: Utf8PathBuf| {
|
let style_from_file = |file: Utf8PathBuf| {
|
||||||
if file.is_dir {
|
if file.is_dir {
|
||||||
Span::styled(file.path, directory_color)
|
Span::styled(file.path, directory_color)
|
||||||
|
@ -664,9 +685,10 @@ pub mod completers {
|
||||||
files.sort_unstable_by(|(_, path1), (_, path2)| path1.content.cmp(&path2.content));
|
files.sort_unstable_by(|(_, path1), (_, path2)| path1.content.cmp(&path2.content));
|
||||||
files
|
files
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
||||||
|
@ -680,7 +702,7 @@ pub mod completers {
|
||||||
.collect()
|
.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(|| {
|
static PROGRAMS_IN_PATH: Lazy<BTreeSet<String>> = Lazy::new(|| {
|
||||||
// Go through the entire PATH and read all files into a set.
|
// Go through the entire PATH and read all files into a set.
|
||||||
let Some(path) = std::env::var_os("PATH") else {
|
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.
|
/// 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() {
|
let token = match Tokenizer::new(input, false).last() {
|
||||||
Some(token) => token.unwrap(),
|
Some(token) => token.unwrap(),
|
||||||
None => return filename(editor, input),
|
None => return filename(editor, input),
|
||||||
|
@ -716,26 +738,28 @@ pub mod completers {
|
||||||
|
|
||||||
let offset = token.content_start;
|
let offset = token.content_start;
|
||||||
|
|
||||||
let mut completions = filename(editor, &input[offset..]);
|
filename(editor, &input[offset..]).map(move |mut completions| {
|
||||||
for completion in completions.iter_mut() {
|
for completion in completions.iter_mut() {
|
||||||
completion.0.start += offset;
|
completion.0.start += offset;
|
||||||
}
|
}
|
||||||
completions
|
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);
|
let (command, args, complete_command) = command_line::split(input);
|
||||||
|
|
||||||
if complete_command {
|
if complete_command {
|
||||||
return program(editor, command);
|
return program(editor, command);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut completions = repeating_filenames(editor, args);
|
let len = command.len();
|
||||||
|
repeating_filenames(editor, args).map(move |mut completions| {
|
||||||
for completion in completions.iter_mut() {
|
for completion in completions.iter_mut() {
|
||||||
// + 1 for separator between `command` and `args`
|
// + 1 for separator between `command` and `args`
|
||||||
completion.0.start += command.len() + 1;
|
completion.0.start += len + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
completions
|
completions
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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, TaskController, TaskHandle};
|
||||||
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::text::Span;
|
use tui::text::Span;
|
||||||
use tui::widgets::{Block, Widget};
|
use tui::widgets::{Block, Widget};
|
||||||
|
@ -19,10 +23,39 @@ use helix_view::{
|
||||||
Editor,
|
Editor,
|
||||||
};
|
};
|
||||||
|
|
||||||
type PromptCharHandler = Box<dyn Fn(&mut Prompt, char, &Context)>;
|
|
||||||
|
|
||||||
pub type Completion = (RangeFrom<usize>, Span<'static>);
|
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)>;
|
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>>>;
|
||||||
|
|
||||||
|
@ -36,11 +69,14 @@ pub struct Prompt {
|
||||||
truncate_start: bool,
|
truncate_start: bool,
|
||||||
truncate_end: 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>,
|
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>,
|
||||||
|
@ -81,7 +117,7 @@ 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 {
|
||||||
|
@ -92,11 +128,14 @@ impl Prompt {
|
||||||
anchor: 0,
|
anchor: 0,
|
||||||
truncate_start: false,
|
truncate_start: false,
|
||||||
truncate_end: false,
|
truncate_end: false,
|
||||||
|
completion_fn: Box::new(completion_fn),
|
||||||
|
completion_hook: CompletionHandler {}.spawn(),
|
||||||
|
task_controller: TaskController::new(),
|
||||||
|
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,
|
||||||
|
@ -153,8 +192,33 @@ impl Prompt {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn recalculate_completion(&mut self, editor: &Editor) {
|
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.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
|
/// Compute the cursor position after applying movement
|
||||||
|
@ -394,6 +458,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;
|
||||||
|
@ -406,7 +480,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…
Reference in New Issue