diff --git a/helix-core/src/command_line.rs b/helix-core/src/command_line.rs index 8e209d618..f1230ba0d 100644 --- a/helix-core/src/command_line.rs +++ b/helix-core/src/command_line.rs @@ -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)] diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index c35ff714a..7adfc2d35 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -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> { Some(Cow::Owned(doc)) } -fn complete_command_line(editor: &Editor, input: &str) -> Vec { +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 Vec { +) -> 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 { +) -> 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 { +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 Vec { +fn complete_expansion_kind(content: &str, offset: usize) -> CompletionResult { use command_line::ExpansionKind; fuzzy_match( diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 47b046c9d..e66bcf133 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -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, - completion_fn: impl FnMut(&Editor, &str) -> Vec + '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, - completion_fn: impl FnMut(&Editor, &str) -> Vec + '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, - completion_fn: impl FnMut(&Editor, &str) -> Vec + '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, - completion_fn: impl FnMut(&Editor, &str) -> Vec + '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; - - pub fn none(_editor: &Editor, _input: &str) -> Vec { - Vec::new() + pub enum CompletionResult { + Immediate(Vec), + Callback(Box Vec + Send + Sync>), } - pub fn buffer(editor: &Editor, input: &str) -> Vec { + fn callback(f: impl FnOnce() -> Vec + Send + Sync + 'static) -> CompletionResult { + CompletionResult::Callback(Box::new(f)) + } + + impl CompletionResult { + pub fn map( + self, + f: impl FnOnce(Vec) -> Vec + Send + Sync + 'static, + ) -> CompletionResult { + match self { + CompletionResult::Immediate(v) => CompletionResult::Immediate(f(v)), + CompletionResult::Callback(v) => callback(move || f(v())), + } + } + } + + impl FromIterator for CompletionResult { + fn from_iter>(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 { - 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 { + 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 { + 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 { + pub fn setting(_editor: &Editor, input: &str) -> CompletionResult { static KEYS: Lazy> = 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 { + 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 { + ) -> 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 { + 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 { + 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 { + 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 { + ) -> 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 + ) -> 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. + // ~ -> -> /home/user + // ~/ -> -> ~/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. - // ~ -> -> /home/user - // ~/ -> -> ~/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 { + 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 { + pub fn program(_editor: &Editor, input: &str) -> CompletionResult { static PROGRAMS_IN_PATH: Lazy> = 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 { + 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 { + 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 + }) } } diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index 3c97a93cd..0f84a93af 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -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; - pub type Completion = (RangeFrom, Span<'static>); -type CompletionFn = Box Vec>; +struct CompletionEvent { + cancel: TaskHandle, + callback: Box Vec + Send + Sync>, + send: std::sync::mpsc::SyncSender>, +} + +struct CompletionHandler {} + +impl helix_event::AsyncHook for CompletionHandler { + type Event = CompletionEvent; + + fn handle_event(&mut self, event: CompletionEvent, _: Option) -> Option { + 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.process_async_completion(); + } + }); + None + } + + fn finish_debounce(&mut self) {} +} + +type PromptCharHandler = Box; +type CompletionFn = Box CompletionResult>; type CallbackFn = Box; pub type DocFn = Box Option>>; @@ -36,11 +69,14 @@ pub struct Prompt { truncate_start: bool, truncate_end: bool, // --- + completion_fn: CompletionFn, + completion_hook: tokio::sync::mpsc::Sender, + task_controller: TaskController, + receive_completion: Option>>, completion: Vec, selection: Option, history_register: Option, history_pos: Option, - completion_fn: CompletionFn, callback_fn: CallbackFn, pub doc_fn: DocFn, next_char_handler: Option, @@ -81,7 +117,7 @@ impl Prompt { pub fn new( prompt: Cow<'static, str>, history_register: Option, - completion_fn: impl FnMut(&Editor, &str) -> Vec + '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