diff options
Diffstat (limited to 'helix-term')
-rw-r--r-- | helix-term/src/application.rs | 71 | ||||
-rw-r--r-- | helix-term/src/commands.rs | 42 | ||||
-rw-r--r-- | helix-term/src/compositor.rs | 6 | ||||
-rw-r--r-- | helix-term/src/ui/completion.rs | 30 | ||||
-rw-r--r-- | helix-term/src/ui/editor.rs | 16 | ||||
-rw-r--r-- | helix-term/src/ui/menu.rs | 8 | ||||
-rw-r--r-- | helix-term/src/ui/picker.rs | 6 |
7 files changed, 151 insertions, 28 deletions
diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index b99fccdf..d8ff2a8a 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -102,6 +102,7 @@ impl Application { if !args.files.is_empty() { let first = &args.files[0]; // we know it's not empty if first.is_dir() { + std::env::set_current_dir(&first)?; editor.new_file(Action::VerticalSplit); compositor.push(Box::new(ui::file_picker(first.clone()))); } else { @@ -204,6 +205,11 @@ impl Application { self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback); self.render(); } + _ = &mut self.editor.idle_timer => { + // idle timeout + self.editor.clear_idle_timer(); + self.handle_idle_timeout(); + } } } } @@ -233,6 +239,38 @@ impl Application { } } + pub fn handle_idle_timeout(&mut self) { + use crate::commands::{completion, Context}; + use helix_view::document::Mode; + + if doc_mut!(self.editor).mode != Mode::Insert || !self.config.editor.auto_completion { + return; + } + let editor_view = self + .compositor + .find(std::any::type_name::<ui::EditorView>()) + .expect("expected at least one EditorView"); + let editor_view = editor_view + .as_any_mut() + .downcast_mut::<ui::EditorView>() + .unwrap(); + + if editor_view.completion.is_some() { + return; + } + + let mut cx = Context { + register: None, + editor: &mut self.editor, + jobs: &mut self.jobs, + count: None, + callback: None, + on_next_key_callback: None, + }; + completion(&mut cx); + self.render(); + } + pub fn handle_terminal_events(&mut self, event: Option<Result<Event, crossterm::ErrorKind>>) { let mut cx = crate::compositor::Context { editor: &mut self.editor, @@ -417,14 +455,6 @@ impl Application { server_id: usize, ) { use helix_lsp::{Call, MethodCall, Notification}; - let editor_view = self - .compositor - .find(std::any::type_name::<ui::EditorView>()) - .expect("expected at least one EditorView"); - let editor_view = editor_view - .as_any_mut() - .downcast_mut::<ui::EditorView>() - .unwrap(); match call { Call::Notification(helix_lsp::jsonrpc::Notification { method, params, .. }) => { @@ -534,7 +564,19 @@ impl Application { Notification::LogMessage(params) => { log::info!("window/logMessage: {:?}", params); } - Notification::ProgressMessage(params) => { + Notification::ProgressMessage(params) + if !self + .compositor + .has_component(std::any::type_name::<ui::Prompt>()) => + { + let editor_view = self + .compositor + .find(std::any::type_name::<ui::EditorView>()) + .expect("expected at least one EditorView"); + let editor_view = editor_view + .as_any_mut() + .downcast_mut::<ui::EditorView>() + .unwrap(); let lsp::ProgressParams { token, value } = params; let lsp::ProgressParamsValue::WorkDone(work) = value; @@ -609,6 +651,9 @@ impl Application { self.editor.set_status(status); } } + Notification::ProgressMessage(_params) => { + // do nothing + } } } Call::MethodCall(helix_lsp::jsonrpc::MethodCall { @@ -643,6 +688,14 @@ impl Application { MethodCall::WorkDoneProgressCreate(params) => { self.lsp_progress.create(server_id, params.token); + let editor_view = self + .compositor + .find(std::any::type_name::<ui::EditorView>()) + .expect("expected at least one EditorView"); + let editor_view = editor_view + .as_any_mut() + .downcast_mut::<ui::EditorView>() + .unwrap(); let spinner = editor_view.spinners_mut().get_or_create(server_id); if spinner.is_stopped() { spinner.start(); diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 6a678de1..f3761d7d 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -1310,7 +1310,8 @@ fn global_search(cx: &mut Context) { cx.push_layer(Box::new(prompt)); - let root = find_root(None).unwrap_or_else(|| PathBuf::from("./")); + let current_path = doc_mut!(cx.editor).path().cloned(); + let show_picker = async move { let all_matches: Vec<(usize, PathBuf)> = UnboundedReceiverStream::new(all_matches_rx).collect().await; @@ -1320,14 +1321,19 @@ fn global_search(cx: &mut Context) { editor.set_status("No matches found".to_string()); return; } + let picker = FilePicker::new( all_matches, move |(_line_num, path)| { - path.strip_prefix(&root) - .unwrap_or(path) + let relative_path = helix_core::path::get_relative_path(path) .to_str() .unwrap() - .into() + .to_owned(); + if current_path.as_ref().map(|p| p == path).unwrap_or(false) { + format!("{} (*)", relative_path).into() + } else { + relative_path.into() + } }, move |editor: &mut Editor, (line_num, path), action| { match editor.open(path.into(), action) { @@ -4160,7 +4166,7 @@ fn remove_primary_selection(cx: &mut Context) { doc.set_selection(view.id, selection); } -fn completion(cx: &mut Context) { +pub fn completion(cx: &mut Context) { // trigger on trigger char, or if user calls it // (or on word char typing??) // after it's triggered, if response marked is_incomplete, update on every subsequent keypress @@ -4205,10 +4211,8 @@ fn completion(cx: &mut Context) { }; let offset_encoding = language_server.offset_encoding(); - let cursor = doc - .selection(view.id) - .primary() - .cursor(doc.text().slice(..)); + let text = doc.text().slice(..); + let cursor = doc.selection(view.id).primary().cursor(text); let pos = pos_to_lsp_pos(doc.text(), cursor, offset_encoding); @@ -4216,6 +4220,15 @@ fn completion(cx: &mut Context) { let trigger_offset = cursor; + // TODO: trigger_offset should be the cursor offset but we also need a starting offset from where we want to apply + // completion filtering. For example logger.te| should filter the initial suggestion list with "te". + + use helix_core::chars; + let mut iter = text.chars_at(cursor); + iter.reverse(); + let offset = iter.take_while(|ch| chars::char_is_word(*ch)).count(); + let start_offset = cursor.saturating_sub(offset); + cx.callback( future, move |editor: &mut Editor, @@ -4238,7 +4251,7 @@ fn completion(cx: &mut Context) { }; if items.is_empty() { - editor.set_error("No completion available".to_string()); + // editor.set_error("No completion available".to_string()); return; } let size = compositor.size(); @@ -4246,7 +4259,14 @@ fn completion(cx: &mut Context) { .find(std::any::type_name::<ui::EditorView>()) .unwrap(); if let Some(ui) = ui.as_any_mut().downcast_mut::<ui::EditorView>() { - ui.set_completion(items, offset_encoding, trigger_offset, size); + ui.set_completion( + editor, + items, + offset_encoding, + start_offset, + trigger_offset, + size, + ); }; }, ); diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs index 36e54ede..cad1df05 100644 --- a/helix-term/src/compositor.rs +++ b/helix-term/src/compositor.rs @@ -171,6 +171,12 @@ impl Compositor { (None, CursorKind::Hidden) } + pub fn has_component(&self, type_name: &str) -> bool { + self.layers + .iter() + .any(|component| component.type_name() == type_name) + } + pub fn find(&mut self, type_name: &str) -> Option<&mut dyn Component> { self.layers .iter_mut() diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 6c9e3a80..c75b24f1 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -69,14 +69,18 @@ impl menu::Item for CompletionItem { /// Wraps a Menu. pub struct Completion { popup: Popup<Menu<CompletionItem>>, + start_offset: usize, + #[allow(dead_code)] trigger_offset: usize, // TODO: maintain a completioncontext with trigger kind & trigger char } impl Completion { pub fn new( + editor: &Editor, items: Vec<CompletionItem>, offset_encoding: helix_lsp::OffsetEncoding, + start_offset: usize, trigger_offset: usize, ) -> Self { // let items: Vec<CompletionItem> = Vec::new(); @@ -175,16 +179,22 @@ impl Completion { }; }); let popup = Popup::new(menu); - Self { + let mut completion = Self { popup, + start_offset, trigger_offset, - } + }; + + // need to recompute immediately in case start_offset != trigger_offset + completion.recompute_filter(editor); + + completion } - pub fn update(&mut self, cx: &mut commands::Context) { + pub fn recompute_filter(&mut self, editor: &Editor) { // recompute menu based on matches let menu = self.popup.contents_mut(); - let (view, doc) = current!(cx.editor); + let (view, doc) = current_ref!(editor); // cx.hooks() // cx.add_hook(enum type, ||) @@ -200,14 +210,22 @@ impl Completion { .selection(view.id) .primary() .cursor(doc.text().slice(..)); - if self.trigger_offset <= cursor { - let fragment = doc.text().slice(self.trigger_offset..cursor); + if self.start_offset <= cursor { + let fragment = doc.text().slice(self.start_offset..cursor); let text = Cow::from(fragment); // TODO: logic is same as ui/picker menu.score(&text); + } else { + // we backspaced before the start offset, clear the menu + // this will cause the editor to remove the completion popup + menu.clear(); } } + pub fn update(&mut self, cx: &mut commands::Context) { + self.recompute_filter(cx.editor) + } + pub fn is_empty(&self) -> bool { self.popup.contents().is_empty() } diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 128fe948..037f04b8 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -37,7 +37,7 @@ pub struct EditorView { keymaps: Keymaps, on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>, last_insert: (commands::Command, Vec<KeyEvent>), - completion: Option<Completion>, + pub(crate) completion: Option<Completion>, spinners: ProgressSpinners, autoinfo: Option<Info>, } @@ -984,12 +984,21 @@ impl EditorView { pub fn set_completion( &mut self, + editor: &Editor, items: Vec<helix_lsp::lsp::CompletionItem>, offset_encoding: helix_lsp::OffsetEncoding, + start_offset: usize, trigger_offset: usize, size: Rect, ) { - let mut completion = Completion::new(items, offset_encoding, trigger_offset); + let mut completion = + Completion::new(editor, items, offset_encoding, start_offset, trigger_offset); + + if completion.is_empty() { + // skip if we got no completion results + return; + } + // TODO : propagate required size on resize to completion too completion.required_size((size.width, size.height)); self.completion = Some(completion); @@ -1211,6 +1220,7 @@ impl Component for EditorView { EventResult::Consumed(None) } Event::Key(key) => { + cxt.editor.reset_idle_timer(); let mut key = KeyEvent::from(key); canonicalize_key(&mut key); // clear status @@ -1245,6 +1255,7 @@ impl Component for EditorView { if callback.is_some() { // assume close_fn self.completion = None; + cxt.editor.clear_idle_timer(); // don't retrigger } } } @@ -1258,6 +1269,7 @@ impl Component for EditorView { completion.update(&mut cxt); if completion.is_empty() { self.completion = None; + cxt.editor.clear_idle_timer(); // don't retrigger } } } diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index dab0c34f..055593fd 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -90,6 +90,14 @@ impl<T: Item> Menu<T> { self.recalculate = true; } + pub fn clear(&mut self) { + self.matches.clear(); + + // reset cursor position + self.cursor = None; + self.scroll = 0; + } + pub fn move_up(&mut self) { let len = self.matches.len(); let pos = self.cursor.map_or(0, |i| (i + len.saturating_sub(1)) % len) % len; diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index ee1ec177..341235ee 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -271,12 +271,18 @@ impl<T> Picker<T> { } pub fn move_up(&mut self) { + if self.matches.is_empty() { + return; + } let len = self.matches.len(); let pos = ((self.cursor + len.saturating_sub(1)) % len) % len; self.cursor = pos; } pub fn move_down(&mut self) { + if self.matches.is_empty() { + return; + } let len = self.matches.len(); let pos = (self.cursor + 1) % len; self.cursor = pos; |