aboutsummaryrefslogtreecommitdiff
path: root/helix-term/src
diff options
context:
space:
mode:
Diffstat (limited to 'helix-term/src')
-rw-r--r--helix-term/src/application.rs71
-rw-r--r--helix-term/src/commands.rs42
-rw-r--r--helix-term/src/compositor.rs6
-rw-r--r--helix-term/src/ui/completion.rs30
-rw-r--r--helix-term/src/ui/editor.rs16
-rw-r--r--helix-term/src/ui/menu.rs8
-rw-r--r--helix-term/src/ui/picker.rs6
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;