From 336e41e63aa3401ef1478b1570cd22f747b71322 Mon Sep 17 00:00:00 2001 From: Rose Hogenson Date: Fri, 27 Sep 2024 13:09:20 -0700 Subject: [PATCH] Handle prompt completions with AsyncHook. This change updates prompt completions in the background, to avoid blocking the UI on slow file IO such as over a networked FS. I followed the example of how LSP completions are handled asynchronously. --- helix-term/src/commands.rs | 2 +- helix-term/src/commands/dap.rs | 126 ++++++++++---------- helix-term/src/commands/lsp.rs | 7 +- helix-term/src/commands/typed.rs | 6 +- helix-term/src/ui/mod.rs | 191 ++++++++++++++++-------------- helix-term/src/ui/picker.rs | 2 +- helix-term/src/ui/prompt.rs | 192 ++++++++++++++++++++----------- 7 files changed, 305 insertions(+), 221 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index b1c29378dec67..0c711643fa5b3 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2182,7 +2182,7 @@ fn searcher(cx: &mut Context, direction: Direction) { cx, "search:".into(), Some(reg), - move |_editor: &Editor, input: &str| { + move |input: &str| { completions .iter() .filter(|comp| comp.starts_with(input)) diff --git a/helix-term/src/commands/dap.rs b/helix-term/src/commands/dap.rs index 0b754bc219923..7d3cc283cfd81 100644 --- a/helix-term/src/commands/dap.rs +++ b/helix-term/src/commands/dap.rs @@ -334,12 +334,8 @@ fn debug_parameter_prompt( .to_owned(); let completer = match field_type { - "filename" => |editor: &Editor, input: &str| { - ui::completers::filename_with_git_ignore(editor, input, false) - }, - "directory" => |editor: &Editor, input: &str| { - ui::completers::directory_with_git_ignore(editor, input, false) - }, + "filename" => |input: &str| ui::completers::filename_with_git_ignore(input, false), + "directory" => |input: &str| ui::completers::directory_with_git_ignore(input, false), _ => ui::completers::none, }; @@ -631,35 +627,37 @@ pub fn dap_edit_condition(cx: &mut Context) { None => return, }; let callback = Box::pin(async move { - let call: Callback = Callback::EditorCompositor(Box::new(move |editor, compositor| { - let mut prompt = Prompt::new( - "condition:".into(), - None, - ui::completers::none, - move |cx, input: &str, event: PromptEvent| { - if event != PromptEvent::Validate { - return; - } - - let breakpoints = &mut cx.editor.breakpoints.get_mut(&path).unwrap(); - breakpoints[pos].condition = match input { - "" => None, - input => Some(input.to_owned()), - }; - - let debugger = debugger!(cx.editor); - - if let Err(e) = breakpoints_changed(debugger, path.clone(), breakpoints) { - cx.editor - .set_error(format!("Failed to set breakpoints: {}", e)); - } - }, - ); - if let Some(condition) = breakpoint.condition { - prompt.insert_str(&condition, editor) - } - compositor.push(Box::new(prompt)); - })); + let call: Callback = + Callback::EditorCompositor(Box::new(move |_editor, compositor| { + let mut prompt = Prompt::new( + "condition:".into(), + None, + ui::completers::none, + move |cx, input: &str, event: PromptEvent| { + if event != PromptEvent::Validate { + return; + } + + let breakpoints = &mut cx.editor.breakpoints.get_mut(&path).unwrap(); + breakpoints[pos].condition = match input { + "" => None, + input => Some(input.to_owned()), + }; + + let debugger = debugger!(cx.editor); + + if let Err(e) = breakpoints_changed(debugger, path.clone(), breakpoints) + { + cx.editor + .set_error(format!("Failed to set breakpoints: {}", e)); + } + }, + ); + if let Some(condition) = breakpoint.condition { + prompt.insert_str(&condition) + } + compositor.push(Box::new(prompt)); + })); Ok(call) }); cx.jobs.callback(callback); @@ -673,34 +671,36 @@ pub fn dap_edit_log(cx: &mut Context) { None => return, }; let callback = Box::pin(async move { - let call: Callback = Callback::EditorCompositor(Box::new(move |editor, compositor| { - let mut prompt = Prompt::new( - "log-message:".into(), - None, - ui::completers::none, - move |cx, input: &str, event: PromptEvent| { - if event != PromptEvent::Validate { - return; - } - - let breakpoints = &mut cx.editor.breakpoints.get_mut(&path).unwrap(); - breakpoints[pos].log_message = match input { - "" => None, - input => Some(input.to_owned()), - }; - - let debugger = debugger!(cx.editor); - if let Err(e) = breakpoints_changed(debugger, path.clone(), breakpoints) { - cx.editor - .set_error(format!("Failed to set breakpoints: {}", e)); - } - }, - ); - if let Some(log_message) = breakpoint.log_message { - prompt.insert_str(&log_message, editor); - } - compositor.push(Box::new(prompt)); - })); + let call: Callback = + Callback::EditorCompositor(Box::new(move |_editor, compositor| { + let mut prompt = Prompt::new( + "log-message:".into(), + None, + ui::completers::none, + move |cx, input: &str, event: PromptEvent| { + if event != PromptEvent::Validate { + return; + } + + let breakpoints = &mut cx.editor.breakpoints.get_mut(&path).unwrap(); + breakpoints[pos].log_message = match input { + "" => None, + input => Some(input.to_owned()), + }; + + let debugger = debugger!(cx.editor); + if let Err(e) = breakpoints_changed(debugger, path.clone(), breakpoints) + { + cx.editor + .set_error(format!("Failed to set breakpoints: {}", e)); + } + }, + ); + if let Some(log_message) = breakpoint.log_message { + prompt.insert_str(&log_message); + } + compositor.push(Box::new(prompt)); + })); Ok(call) }); cx.jobs.callback(callback); diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 93ac2a849f79a..55ba5dce5b444 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -1125,7 +1125,6 @@ pub fn rename_symbol(cx: &mut Context) { } fn create_rename_prompt( - editor: &Editor, prefill: String, history_register: Option, language_server_id: Option, @@ -1163,7 +1162,7 @@ pub fn rename_symbol(cx: &mut Context) { } }, ) - .with_line(prefill, editor); + .with_line(prefill); Box::new(prompt) } @@ -1212,14 +1211,14 @@ pub fn rename_symbol(cx: &mut Context) { } }; - let prompt = create_rename_prompt(editor, prefill, history_register, Some(ls_id)); + let prompt = create_rename_prompt(prefill, history_register, Some(ls_id)); compositor.push(prompt); }, ); } else { let prefill = get_prefill_from_word_boundary(cx.editor); - let prompt = create_rename_prompt(cx.editor, prefill, history_register, None); + let prompt = create_rename_prompt(prefill, history_register, None); cx.push_layer(prompt); } } diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 7ad0369fc1bd1..0c6b8544e48f9 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -3159,7 +3159,7 @@ pub(super) fn command_mode(cx: &mut Context) { let mut prompt = Prompt::new( ":".into(), Some(':'), - |editor: &Editor, input: &str| { + |input: &str| { let shellwords = Shellwords::from(input); let words = shellwords.words(); @@ -3187,7 +3187,7 @@ pub(super) fn command_mode(cx: &mut Context) { .get(&words[0] as &str) .map(|tc| tc.completer_for_argument_number(argument_number)) { - completer(editor, word) + completer(word) .into_iter() .map(|(range, file)| { let file = shellwords::escape(file); @@ -3247,7 +3247,7 @@ pub(super) fn command_mode(cx: &mut Context) { }); // Calculate initial completion - prompt.recalculate_completion(cx.editor); + prompt.recalculate_completion(); cx.push_layer(Box::new(prompt)); } diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 6a3e198c10515..4c8def05be7bb 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -36,12 +36,12 @@ 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(&str) -> Vec + Send + Sync + 'static, callback_fn: impl FnMut(&mut crate::compositor::Context, &str, PromptEvent) + 'static, ) { let mut prompt = Prompt::new(prompt, history_register, completion_fn, callback_fn); // Calculate the initial completion - prompt.recalculate_completion(cx.editor); + prompt.recalculate_completion(); cx.push_layer(Box::new(prompt)); } @@ -50,11 +50,10 @@ pub fn prompt_with_input( prompt: std::borrow::Cow<'static, str>, input: String, history_register: Option, - completion_fn: impl FnMut(&Editor, &str) -> Vec + 'static, + completion_fn: impl FnMut(&str) -> Vec + Send + Sync + '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); + let prompt = Prompt::new(prompt, history_register, completion_fn, callback_fn).with_line(input); cx.push_layer(Box::new(prompt)); } @@ -62,7 +61,7 @@ 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(&str) -> Vec + Send + Sync + 'static, fun: impl Fn(&mut crate::compositor::Context, rope::Regex, PromptEvent) + 'static, ) { raw_regex_prompt( @@ -77,7 +76,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(&str) -> Vec + Send + Sync + 'static, fun: impl Fn(&mut crate::compositor::Context, rope::Regex, &str, PromptEvent) + 'static, ) { let (view, doc) = current!(cx.editor); @@ -166,7 +165,7 @@ pub fn raw_regex_prompt( ) .with_language("regex", std::sync::Arc::clone(&cx.editor.syn_loader)); // Calculate initial completion - prompt.recalculate_completion(cx.editor); + prompt.recalculate_completion(); // prompt cx.push_layer(Box::new(prompt)); } @@ -266,35 +265,45 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi } pub mod completers { + use crate::job; use crate::ui::prompt::Completion; use helix_core::fuzzy::fuzzy_match; use helix_core::syntax::LanguageServerFeature; use helix_view::document::SCRATCH_BUFFER_NAME; + use helix_view::editor::Config; use helix_view::theme; - use helix_view::{editor::Config, Editor}; use once_cell::sync::Lazy; use std::borrow::Cow; - pub type Completer = fn(&Editor, &str) -> Vec; + pub type Completer = fn(&str) -> Vec; - pub fn none(_editor: &Editor, _input: &str) -> Vec { + pub fn none(_input: &str) -> Vec { Vec::new() } - pub fn buffer(editor: &Editor, input: &str) -> Vec { - let names = editor.documents.values().map(|doc| { - doc.relative_path() - .map(|p| p.display().to_string().into()) - .unwrap_or_else(|| Cow::from(SCRATCH_BUFFER_NAME)) - }); + pub fn buffer(input: &str) -> Vec { + let (result_send, result_recv) = std::sync::mpsc::sync_channel(1); + let input = String::from(input); + job::dispatch_blocking(move |editor, _compositor| { + let names = editor.documents.values().map(|doc| { + doc.relative_path() + .map(|p| p.display().to_string().into()) + .unwrap_or_else(|| Cow::from(SCRATCH_BUFFER_NAME)) + }); - fuzzy_match(input, names, true) - .into_iter() - .map(|(name, _)| ((0..), name)) - .collect() + result_send + .send( + fuzzy_match(&input, names, true) + .into_iter() + .map(|(name, _)| ((0..), name)) + .collect(), + ) + .unwrap(); + }); + result_recv.recv().unwrap() } - pub fn theme(_editor: &Editor, input: &str) -> Vec { + pub fn theme(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"))); @@ -326,7 +335,7 @@ pub mod completers { } } - pub fn setting(_editor: &Editor, input: &str) -> Vec { + pub fn setting(input: &str) -> Vec { static KEYS: Lazy> = Lazy::new(|| { let mut keys = Vec::new(); let json = serde_json::json!(Config::default()); @@ -340,16 +349,12 @@ pub mod completers { .collect() } - pub fn filename(editor: &Editor, input: &str) -> Vec { - filename_with_git_ignore(editor, input, true) + pub fn filename(input: &str) -> Vec { + filename_with_git_ignore(input, true) } - pub fn filename_with_git_ignore( - editor: &Editor, - input: &str, - git_ignore: bool, - ) -> Vec { - filename_impl(editor, input, git_ignore, |entry| { + pub fn filename_with_git_ignore(input: &str, git_ignore: bool) -> Vec { + filename_impl(input, git_ignore, |entry| { let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir()); if is_dir { @@ -360,47 +365,61 @@ pub mod completers { }) } - pub fn language(editor: &Editor, input: &str) -> Vec { - let text: String = "text".into(); - - let loader = editor.syn_loader.load(); - let language_ids = loader - .language_configs() - .map(|config| &config.language_id) - .chain(std::iter::once(&text)); - - fuzzy_match(input, language_ids, false) - .into_iter() - .map(|(name, _)| ((0..), name.to_owned().into())) - .collect() + pub fn language(input: &str) -> Vec { + let (result_send, result_recv) = std::sync::mpsc::sync_channel(1); + let input = String::from(input); + job::dispatch_blocking(move |editor, _compositor| { + let text: String = "text".into(); + + let loader = editor.syn_loader.load(); + let language_ids = loader + .language_configs() + .map(|config| &config.language_id) + .chain(std::iter::once(&text)); + + result_send + .send( + fuzzy_match(&input, language_ids, false) + .into_iter() + .map(|(name, _)| ((0..), name.to_owned().into())) + .collect(), + ) + .unwrap(); + }); + result_recv.recv().unwrap() } - pub fn lsp_workspace_command(editor: &Editor, input: &str) -> Vec { - let commands = doc!(editor) - .language_servers_with_feature(LanguageServerFeature::WorkspaceCommand) - .flat_map(|ls| { - ls.capabilities() - .execute_command_provider - .iter() - .flat_map(|options| options.commands.iter()) - }); - - fuzzy_match(input, commands, false) - .into_iter() - .map(|(name, _)| ((0..), name.to_owned().into())) - .collect() + pub fn lsp_workspace_command(input: &str) -> Vec { + let (result_send, result_recv) = std::sync::mpsc::sync_channel(1); + let input = String::from(input); + job::dispatch_blocking(move |editor, _compositor| { + let commands = doc!(editor) + .language_servers_with_feature(LanguageServerFeature::WorkspaceCommand) + .flat_map(|ls| { + ls.capabilities() + .execute_command_provider + .iter() + .flat_map(|options| options.commands.iter()) + }); + + result_send + .send( + fuzzy_match(&input, commands, false) + .into_iter() + .map(|(name, _)| ((0..), name.to_owned().into())) + .collect(), + ) + .unwrap(); + }); + result_recv.recv().unwrap() } - pub fn directory(editor: &Editor, input: &str) -> Vec { - directory_with_git_ignore(editor, input, true) + pub fn directory(input: &str) -> Vec { + directory_with_git_ignore(input, true) } - pub fn directory_with_git_ignore( - editor: &Editor, - input: &str, - git_ignore: bool, - ) -> Vec { - filename_impl(editor, input, git_ignore, |entry| { + pub fn directory_with_git_ignore(input: &str, git_ignore: bool) -> Vec { + filename_impl(input, git_ignore, |entry| { let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir()); if is_dir { @@ -423,12 +442,7 @@ pub mod completers { } // TODO: we could return an iter/lazy thing so it can fetch as many as it needs. - fn filename_impl( - _editor: &Editor, - input: &str, - git_ignore: bool, - filter_fn: F, - ) -> Vec + fn filename_impl(input: &str, git_ignore: bool, filter_fn: F) -> Vec where F: Fn(&ignore::DirEntry) -> FileMatch, { @@ -522,17 +536,26 @@ pub mod completers { } } - pub fn register(editor: &Editor, input: &str) -> Vec { - let iter = editor - .registers - .iter_preview() - // Exclude special registers that shouldn't be written to - .filter(|(ch, _)| !matches!(ch, '%' | '#' | '.')) - .map(|(ch, _)| ch.to_string()); - - fuzzy_match(input, iter, false) - .into_iter() - .map(|(name, _)| ((0..), name.into())) - .collect() + pub fn register(input: &str) -> Vec { + let (result_send, result_recv) = std::sync::mpsc::sync_channel(1); + let input = String::from(input); + job::dispatch_blocking(move |editor, _compositor| { + let iter = editor + .registers + .iter_preview() + // Exclude special registers that shouldn't be written to + .filter(|(ch, _)| !matches!(ch, '%' | '#' | '.')) + .map(|(ch, _)| ch.to_string()); + + result_send + .send( + fuzzy_match(&input, iter, false) + .into_iter() + .map(|(name, _)| ((0..), name.into())) + .collect(), + ) + .unwrap(); + }); + result_recv.recv().unwrap() } } diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 82fe9689164de..afcf7661108f0 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -1040,7 +1040,7 @@ impl Component for Picker; pub type Completion = (RangeFrom, Cow<'static, str>); -type CompletionFn = Box Vec>; +type CompletionFn = Box Vec + Sync + Send>; + +struct CompletionHandler { + completion_fn: CompletionFn, +} + +struct CompletionEvent { + cancel: CancelRx, + line: String, + send: std::sync::mpsc::SyncSender>, +} + +impl helix_event::AsyncHook for CompletionHandler { + type Event = CompletionEvent; + + fn handle_event(&mut self, mut event: CompletionEvent, _: Option) -> Option { + let Err(tokio::sync::oneshot::error::TryRecvError::Empty) = event.cancel.try_recv() else { + return None; + }; + let completions = (self.completion_fn)(&event.line); + if event.send.send(completions).is_err() { + return None; + } + helix_event::request_redraw(); + None + } + + fn finish_debounce(&mut self) {} +} + +type PromptCharHandler = Box; type CallbackFn = Box; pub type DocFn = Box Option>>; @@ -28,11 +60,13 @@ pub struct Prompt { prompt: Cow<'static, str>, line: String, cursor: usize, - completion: Vec, + completion_req: Option, + request_completion: tokio::sync::mpsc::Sender, + receive_completion: std::sync::mpsc::Receiver>, + completion: Mutex>, selection: Option, history_register: Option, history_pos: Option, - completion_fn: CompletionFn, callback_fn: CallbackFn, pub doc_fn: DocFn, next_char_handler: Option, @@ -73,18 +107,25 @@ impl Prompt { pub fn new( prompt: Cow<'static, str>, history_register: Option, - completion_fn: impl FnMut(&Editor, &str) -> Vec + 'static, + completion_fn: impl FnMut(&str) -> Vec + Sync + Send + 'static, callback_fn: impl FnMut(&mut Context, &str, PromptEvent) + 'static, ) -> Self { + let completion_handler = CompletionHandler { + completion_fn: Box::new(completion_fn), + }; + let request_completion = completion_handler.spawn(); + let (_, receive_completion) = std::sync::mpsc::sync_channel(0); Self { prompt, line: String::new(), cursor: 0, - completion: Vec::new(), + completion_req: None, + request_completion, + receive_completion, + completion: Mutex::new(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, @@ -98,16 +139,16 @@ impl Prompt { self.cursor } - pub fn with_line(mut self, line: String, editor: &Editor) -> Self { - self.set_line(line, editor); + pub fn with_line(mut self, line: String) -> Self { + self.set_line(line); self } - pub fn set_line(&mut self, line: String, editor: &Editor) { + pub fn set_line(&mut self, line: String) { let cursor = line.len(); self.line = line; self.cursor = cursor; - self.recalculate_completion(editor); + self.recalculate_completion(); } pub fn with_language( @@ -140,9 +181,27 @@ impl Prompt { .and_then(|reg| editor.registers.first(reg, editor)) } - pub fn recalculate_completion(&mut self, editor: &Editor) { + pub fn recalculate_completion(&mut self) { self.exit_selection(); - self.completion = (self.completion_fn)(editor, &self.line); + let (cancel_tx, cancel_rx) = helix_event::cancelation(); + let (send_completion, recv_completion) = std::sync::mpsc::sync_channel(1); + helix_event::send_blocking( + &self.request_completion, + CompletionEvent { + cancel: cancel_rx, + line: self.line.clone(), + send: send_completion, + }, + ); + // To avoid flicker, give the completion handler a small timeout to + // complete immediately. + if let Ok(completion) = recv_completion.recv_timeout(Duration::from_millis(100)) { + *self.completion.lock().unwrap() = completion; + return; + } + self.completion.lock().unwrap().clear(); + self.completion_req = Some(cancel_tx); + self.receive_completion = recv_completion; } /// Compute the cursor position after applying movement @@ -259,13 +318,13 @@ impl Prompt { if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) { self.cursor = pos; } - self.recalculate_completion(cx.editor); + self.recalculate_completion(); } - pub fn insert_str(&mut self, s: &str, editor: &Editor) { + pub fn insert_str(&mut self, s: &str) { self.line.insert_str(self.cursor, s); self.cursor += s.len(); - self.recalculate_completion(editor); + self.recalculate_completion(); } pub fn move_cursor(&mut self, movement: Movement) { @@ -281,55 +340,55 @@ impl Prompt { self.cursor = self.line.len(); } - pub fn delete_char_backwards(&mut self, editor: &Editor) { + pub fn delete_char_backwards(&mut self) { let pos = self.eval_movement(Movement::BackwardChar(1)); self.line.replace_range(pos..self.cursor, ""); self.cursor = pos; - self.recalculate_completion(editor); + self.recalculate_completion(); } - pub fn delete_char_forwards(&mut self, editor: &Editor) { + pub fn delete_char_forwards(&mut self) { let pos = self.eval_movement(Movement::ForwardChar(1)); self.line.replace_range(self.cursor..pos, ""); - self.recalculate_completion(editor); + self.recalculate_completion(); } - pub fn delete_word_backwards(&mut self, editor: &Editor) { + pub fn delete_word_backwards(&mut self) { let pos = self.eval_movement(Movement::BackwardWord(1)); self.line.replace_range(pos..self.cursor, ""); self.cursor = pos; - self.recalculate_completion(editor); + self.recalculate_completion(); } - pub fn delete_word_forwards(&mut self, editor: &Editor) { + pub fn delete_word_forwards(&mut self) { let pos = self.eval_movement(Movement::ForwardWord(1)); self.line.replace_range(self.cursor..pos, ""); - self.recalculate_completion(editor); + self.recalculate_completion(); } - pub fn kill_to_start_of_line(&mut self, editor: &Editor) { + pub fn kill_to_start_of_line(&mut self) { let pos = self.eval_movement(Movement::StartOfLine); self.line.replace_range(pos..self.cursor, ""); self.cursor = pos; - self.recalculate_completion(editor); + self.recalculate_completion(); } - pub fn kill_to_end_of_line(&mut self, editor: &Editor) { + pub fn kill_to_end_of_line(&mut self) { let pos = self.eval_movement(Movement::EndOfLine); self.line.replace_range(self.cursor..pos, ""); - self.recalculate_completion(editor); + self.recalculate_completion(); } - pub fn clear(&mut self, editor: &Editor) { + pub fn clear(&mut self) { self.line.clear(); self.cursor = 0; - self.recalculate_completion(editor); + self.recalculate_completion(); } pub fn change_history( @@ -363,26 +422,27 @@ impl Prompt { self.move_end(); (self.callback_fn)(cx, &self.line, PromptEvent::Update); - self.recalculate_completion(cx.editor); + self.recalculate_completion(); } pub fn change_completion_selection(&mut self, direction: CompletionDirection) { - if self.completion.is_empty() { - return; - } - - let index = match direction { - CompletionDirection::Forward => self.selection.map_or(0, |i| i + 1), - CompletionDirection::Backward => { - self.selection.unwrap_or(0) + self.completion.len() - 1 + { + let completion = self.completion.lock().unwrap(); + if completion.is_empty() { + return; } - } % self.completion.len(); - self.selection = Some(index); + let index = match direction { + CompletionDirection::Forward => self.selection.map_or(0, |i| i + 1), + CompletionDirection::Backward => self.selection.unwrap_or(0) + completion.len() - 1, + } % completion.len(); - let (range, item) = &self.completion[index]; + self.selection = Some(index); - self.line.replace_range(range.clone(), item); + let (range, item) = &completion[index]; + + self.line.replace_range(range.clone(), item); + } self.move_end(); } @@ -402,10 +462,12 @@ 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 + if let Ok(completion) = self.receive_completion.try_recv() { + *self.completion.lock().unwrap() = completion; + } + let completion = self.completion.lock().unwrap().clone(); - let max_len = self - .completion + let max_len = completion .iter() .map(|(_, completion)| completion.len() as u16) .max() @@ -415,7 +477,7 @@ impl Prompt { let cols = std::cmp::max(1, area.width / max_len); let col_width = (area.width.saturating_sub(cols)) / cols; - let height = ((self.completion.len() as u16 + cols - 1) / cols) + let height = ((completion.len() as u16 + cols - 1) / cols) .min(10) // at most 10 rows (or less) .min(area.height.saturating_sub(1)); @@ -426,7 +488,7 @@ impl Prompt { height, ); - if completion_area.height > 0 && !self.completion.is_empty() { + if completion_area.height > 0 && !completion.is_empty() { let area = completion_area; let background = theme.get("ui.menu"); @@ -442,8 +504,7 @@ impl Prompt { let mut row = 0; let mut col = 0; - for (i, (_range, completion)) in - self.completion.iter().enumerate().skip(offset).take(items) + for (i, (_range, completion)) in completion.iter().enumerate().skip(offset).take(items) { let color = if Some(i) == self.selection { selected_color // TODO: just invert bg @@ -526,8 +587,8 @@ impl Component for Prompt { fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult { let event = match event { Event::Paste(data) => { - self.insert_str(data, cx.editor); - self.recalculate_completion(cx.editor); + self.insert_str(data); + self.recalculate_completion(); return EventResult::Consumed(None); } Event::Key(event) => *event, @@ -552,27 +613,27 @@ impl Component for Prompt { ctrl!('e') | key!(End) => self.move_end(), ctrl!('a') | key!(Home) => self.move_start(), ctrl!('w') | alt!(Backspace) | ctrl!(Backspace) => { - self.delete_word_backwards(cx.editor); + self.delete_word_backwards(); (self.callback_fn)(cx, &self.line, PromptEvent::Update); } alt!('d') | alt!(Delete) | ctrl!(Delete) => { - self.delete_word_forwards(cx.editor); + self.delete_word_forwards(); (self.callback_fn)(cx, &self.line, PromptEvent::Update); } ctrl!('k') => { - self.kill_to_end_of_line(cx.editor); + self.kill_to_end_of_line(); (self.callback_fn)(cx, &self.line, PromptEvent::Update); } ctrl!('u') => { - self.kill_to_start_of_line(cx.editor); + self.kill_to_start_of_line(); (self.callback_fn)(cx, &self.line, PromptEvent::Update); } ctrl!('h') | key!(Backspace) | shift!(Backspace) => { - self.delete_char_backwards(cx.editor); + self.delete_char_backwards(); (self.callback_fn)(cx, &self.line, PromptEvent::Update); } ctrl!('d') | key!(Delete) => { - self.delete_char_forwards(cx.editor); + self.delete_char_forwards(); (self.callback_fn)(cx, &self.line, PromptEvent::Update); } ctrl!('s') => { @@ -589,13 +650,13 @@ impl Component for Prompt { ); let line = text.slice(range.from()..range.to()).to_string(); if !line.is_empty() { - self.insert_str(line.as_str(), cx.editor); + self.insert_str(line.as_str()); (self.callback_fn)(cx, &self.line, PromptEvent::Update); } } key!(Enter) => { if self.selection.is_some() && self.line.ends_with(std::path::MAIN_SEPARATOR) { - self.recalculate_completion(cx.editor); + self.recalculate_completion(); } else { let last_item = self .first_history_completion(cx.editor) @@ -638,8 +699,10 @@ impl Component for Prompt { key!(Tab) => { self.change_completion_selection(CompletionDirection::Forward); // if single completion candidate is a directory list content in completion - if self.completion.len() == 1 && self.line.ends_with(std::path::MAIN_SEPARATOR) { - self.recalculate_completion(cx.editor); + if self.completion.lock().unwrap().len() == 1 + && self.line.ends_with(std::path::MAIN_SEPARATOR) + { + self.recalculate_completion(); } (self.callback_fn)(cx, &self.line, PromptEvent::Update) } @@ -649,7 +712,7 @@ impl Component for Prompt { } ctrl!('q') => self.exit_selection(), ctrl!('r') => { - self.completion = cx + *self.completion.lock().unwrap() = cx .editor .registers .iter_preview() @@ -662,7 +725,6 @@ impl Component for Prompt { .registers .first(c, context.editor) .unwrap_or_default(), - context.editor, ); })); (self.callback_fn)(cx, &self.line, PromptEvent::Update);