diff options
Diffstat (limited to 'helix-term/src/ui')
-rw-r--r-- | helix-term/src/ui/completion.rs | 12 | ||||
-rw-r--r-- | helix-term/src/ui/editor.rs | 191 | ||||
-rw-r--r-- | helix-term/src/ui/fuzzy_match.rs | 74 | ||||
-rw-r--r-- | helix-term/src/ui/fuzzy_match/test.rs | 47 | ||||
-rw-r--r-- | helix-term/src/ui/lsp.rs | 3 | ||||
-rw-r--r-- | helix-term/src/ui/menu.rs | 2 | ||||
-rw-r--r-- | helix-term/src/ui/mod.rs | 42 | ||||
-rw-r--r-- | helix-term/src/ui/picker.rs | 64 | ||||
-rw-r--r-- | helix-term/src/ui/statusline.rs | 18 |
9 files changed, 356 insertions, 97 deletions
diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 2d7d4f92..7348dcf4 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -1,5 +1,5 @@ use crate::compositor::{Component, Context, Event, EventResult}; -use helix_view::editor::CompleteAction; +use helix_view::{apply_transaction, editor::CompleteAction}; use tui::buffer::Buffer as Surface; use tui::text::Spans; @@ -143,11 +143,11 @@ impl Completion { let (view, doc) = current!(editor); // if more text was entered, remove it - doc.restore(view.id); + doc.restore(view); match event { PromptEvent::Abort => { - doc.restore(view.id); + doc.restore(view); editor.last_completion = None; } PromptEvent::Update => { @@ -164,7 +164,7 @@ impl Completion { // initialize a savepoint doc.savepoint(); - doc.apply(&transaction, view.id); + apply_transaction(&transaction, doc, view); editor.last_completion = Some(CompleteAction { trigger_offset, @@ -183,7 +183,7 @@ impl Completion { trigger_offset, ); - doc.apply(&transaction, view.id); + apply_transaction(&transaction, doc, view); editor.last_completion = Some(CompleteAction { trigger_offset, @@ -213,7 +213,7 @@ impl Completion { additional_edits.clone(), offset_encoding, // TODO: should probably transcode in Client ); - doc.apply(&transaction, view.id); + apply_transaction(&transaction, doc, view); } } } diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 7cb29c3b..3cd2130a 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -13,9 +13,10 @@ use helix_core::{ movement::Direction, syntax::{self, HighlightEvent}, unicode::width::UnicodeWidthStr, - LineEnding, Position, Range, Selection, Transaction, + visual_coords_at_pos, LineEnding, Position, Range, Selection, Transaction, }; use helix_view::{ + apply_transaction, document::{Mode, SCRATCH_BUFFER_NAME}, editor::{CompleteAction, CursorShapeConfig}, graphics::{Color, CursorKind, Modifier, Rect, Style}, @@ -23,7 +24,7 @@ use helix_view::{ keyboard::{KeyCode, KeyModifiers}, Document, Editor, Theme, View, }; -use std::{borrow::Cow, path::PathBuf}; +use std::{borrow::Cow, cmp::min, path::PathBuf}; use tui::buffer::Buffer as Surface; @@ -33,6 +34,7 @@ use super::statusline; pub struct EditorView { pub keymaps: Keymaps, on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>, + pseudo_pending: Vec<KeyEvent>, last_insert: (commands::MappableCommand, Vec<InsertEvent>), pub(crate) completion: Option<Completion>, spinners: ProgressSpinners, @@ -56,6 +58,7 @@ impl EditorView { Self { keymaps, on_next_key: None, + pseudo_pending: Vec::new(), last_insert: (commands::MappableCommand::normal_mode, Vec::new()), completion: None, spinners: ProgressSpinners::default(), @@ -116,9 +119,19 @@ impl EditorView { if is_focused && editor.config().cursorline { Self::highlight_cursorline(doc, view, surface, theme); } + if is_focused && editor.config().cursorcolumn { + Self::highlight_cursorcolumn(doc, view, surface, theme); + } - let highlights = Self::doc_syntax_highlights(doc, view.offset, inner.height, theme); - let highlights = syntax::merge(highlights, Self::doc_diagnostics_highlights(doc, theme)); + let mut highlights = Self::doc_syntax_highlights(doc, view.offset, inner.height, theme); + for diagnostic in Self::doc_diagnostics_highlights(doc, theme) { + // Most of the `diagnostic` Vecs are empty most of the time. Skipping + // a merge for any empty Vec saves a significant amount of work. + if diagnostic.is_empty() { + continue; + } + highlights = Box::new(syntax::merge(highlights, diagnostic)); + } let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused { Box::new(syntax::merge( highlights, @@ -262,7 +275,7 @@ impl EditorView { pub fn doc_diagnostics_highlights( doc: &Document, theme: &Theme, - ) -> Vec<(usize, std::ops::Range<usize>)> { + ) -> [Vec<(usize, std::ops::Range<usize>)>; 5] { use helix_core::diagnostic::Severity; let get_scope_of = |scope| { theme @@ -283,22 +296,38 @@ impl EditorView { let error = get_scope_of("diagnostic.error"); let r#default = get_scope_of("diagnostic"); // this is a bit redundant but should be fine - doc.diagnostics() - .iter() - .map(|diagnostic| { - let diagnostic_scope = match diagnostic.severity { - Some(Severity::Info) => info, - Some(Severity::Hint) => hint, - Some(Severity::Warning) => warning, - Some(Severity::Error) => error, - _ => r#default, - }; - ( - diagnostic_scope, - diagnostic.range.start..diagnostic.range.end, - ) - }) - .collect() + let mut default_vec: Vec<(usize, std::ops::Range<usize>)> = Vec::new(); + let mut info_vec = Vec::new(); + let mut hint_vec = Vec::new(); + let mut warning_vec = Vec::new(); + let mut error_vec = Vec::new(); + + for diagnostic in doc.diagnostics() { + // Separate diagnostics into different Vecs by severity. + let (vec, scope) = match diagnostic.severity { + Some(Severity::Info) => (&mut info_vec, info), + Some(Severity::Hint) => (&mut hint_vec, hint), + Some(Severity::Warning) => (&mut warning_vec, warning), + Some(Severity::Error) => (&mut error_vec, error), + _ => (&mut default_vec, r#default), + }; + + // If any diagnostic overlaps ranges with the prior diagnostic, + // merge the two together. Otherwise push a new span. + match vec.last_mut() { + Some((_, range)) if diagnostic.range.start <= range.end => { + // This branch merges overlapping diagnostics, assuming that the current + // diagnostic starts on range.start or later. If this assertion fails, + // we will discard some part of `diagnostic`. This implies that + // `doc.diagnostics()` is not sorted by `diagnostic.range`. + debug_assert!(range.start <= diagnostic.range.start); + range.end = diagnostic.range.end.max(range.end) + } + _ => vec.push((scope, diagnostic.range.start..diagnostic.range.end)), + } + } + + [default_vec, info_vec, hint_vec, warning_vec, error_vec] } /// Get highlight spans for selections in a document view. @@ -399,7 +428,7 @@ impl EditorView { let characters = &whitespace.characters; let mut spans = Vec::new(); - let mut visual_x = 0u16; + let mut visual_x = 0usize; let mut line = 0u16; let tab_width = doc.tab_width(); let tab = if whitespace.render.tab() == WhitespaceRenderValue::All { @@ -436,17 +465,22 @@ impl EditorView { return; } - let starting_indent = (offset.col / tab_width) as u16; - // TODO: limit to a max indent level too. It doesn't cause visual artifacts but it would avoid some - // extra loops if the code is deeply nested. - - for i in starting_indent..(indent_level / tab_width as u16) { - surface.set_string( - viewport.x + (i * tab_width as u16) - offset.col as u16, - viewport.y + line, - &indent_guide_char, - indent_guide_style, - ); + let starting_indent = + (offset.col / tab_width) + config.indent_guides.skip_levels as usize; + + // Don't draw indent guides outside of view + let end_indent = min( + indent_level, + // Add tab_width - 1 to round up, since the first visible + // indent might be a bit after offset.col + offset.col + viewport.width as usize + (tab_width - 1), + ) / tab_width; + + for i in starting_indent..end_indent { + let x = (viewport.x as usize + (i * tab_width) - offset.col) as u16; + let y = viewport.y + line; + debug_assert!(surface.in_bounds(x, y)); + surface.set_string(x, y, &indent_guide_char, indent_guide_style); } }; @@ -488,14 +522,14 @@ impl EditorView { use helix_core::graphemes::{grapheme_width, RopeGraphemes}; for grapheme in RopeGraphemes::new(text) { - let out_of_bounds = visual_x < offset.col as u16 - || visual_x >= viewport.width + offset.col as u16; + let out_of_bounds = offset.col > (visual_x as usize) + || (visual_x as usize) >= viewport.width as usize + offset.col; if LineEnding::from_rope_slice(&grapheme).is_some() { if !out_of_bounds { // we still want to render an empty cell with the style surface.set_string( - viewport.x + visual_x - offset.col as u16, + (viewport.x as usize + visual_x - offset.col) as u16, viewport.y + line, &newline, style.patch(whitespace_style), @@ -543,7 +577,7 @@ impl EditorView { if !out_of_bounds { // if we're offscreen just keep going until we hit a new line surface.set_string( - viewport.x + visual_x - offset.col as u16, + (viewport.x as usize + visual_x - offset.col) as u16, viewport.y + line, display_grapheme, if is_whitespace { @@ -576,7 +610,7 @@ impl EditorView { last_line_indent_level = visual_x; } - visual_x = visual_x.saturating_add(width as u16); + visual_x = visual_x.saturating_add(width); } } } @@ -696,6 +730,7 @@ impl EditorView { let mut offset = 0; let gutter_style = theme.get("ui.gutter"); + let gutter_selected_style = theme.get("ui.gutter.selected"); // avoid lots of small allocations by reusing a text buffer for each line let mut text = String::with_capacity(8); @@ -708,6 +743,12 @@ impl EditorView { let x = viewport.x + offset; let y = viewport.y + i as u16; + let gutter_style = if selected { + gutter_selected_style + } else { + gutter_style + }; + if let Some(style) = gutter(line, selected, &mut text) { surface.set_stringn(x, y, &text, *width, gutter_style.patch(style)); } else { @@ -820,6 +861,53 @@ impl EditorView { } } + /// Apply the highlighting on the columns where a cursor is active + pub fn highlight_cursorcolumn( + doc: &Document, + view: &View, + surface: &mut Surface, + theme: &Theme, + ) { + let text = doc.text().slice(..); + + // Manual fallback behaviour: + // ui.cursorcolumn.{p/s} -> ui.cursorcolumn -> ui.cursorline.{p/s} + let primary_style = theme + .try_get_exact("ui.cursorcolumn.primary") + .or_else(|| theme.try_get_exact("ui.cursorcolumn")) + .unwrap_or_else(|| theme.get("ui.cursorline.primary")); + let secondary_style = theme + .try_get_exact("ui.cursorcolumn.secondary") + .or_else(|| theme.try_get_exact("ui.cursorcolumn")) + .unwrap_or_else(|| theme.get("ui.cursorline.secondary")); + + let inner_area = view.inner_area(); + let offset = view.offset.col; + + let selection = doc.selection(view.id); + let primary = selection.primary(); + for range in selection.iter() { + let is_primary = primary == *range; + + let Position { row: _, col } = + visual_coords_at_pos(text, range.cursor(text), doc.tab_width()); + // if the cursor is horizontally in the view + if col >= offset && inner_area.width > (col - offset) as u16 { + let area = Rect::new( + inner_area.x + (col - offset) as u16, + view.area.y, + 1, + view.area.height, + ); + if is_primary { + surface.set_style(area, primary_style) + } else { + surface.set_style(area, secondary_style) + } + } + } + } + /// Handle events by looking them up in `self.keymaps`. Returns None /// if event was handled (a command was executed or a subkeymap was /// activated). Only KeymapResult::{NotFound, Cancelled} is returned @@ -831,6 +919,7 @@ impl EditorView { event: KeyEvent, ) -> Option<KeymapResult> { let mut last_mode = mode; + self.pseudo_pending.extend(self.keymaps.pending()); let key_result = self.keymaps.get(mode, event); cxt.editor.autoinfo = self.keymaps.sticky().map(|node| node.infobox()); @@ -927,7 +1016,7 @@ impl EditorView { InsertEvent::CompletionApply(compl) => { let (view, doc) = current!(cxt.editor); - doc.restore(view.id); + doc.restore(view); let text = doc.text().slice(..); let cursor = doc.selection(view.id).primary().cursor(text); @@ -941,7 +1030,7 @@ impl EditorView { (shift_position(start), shift_position(end), t) }), ); - doc.apply(&tx, view.id); + apply_transaction(&tx, doc, view); } InsertEvent::TriggerCompletion => { let (_, doc) = current!(cxt.editor); @@ -1005,7 +1094,7 @@ impl EditorView { editor.clear_idle_timer(); // don't retrigger } - pub fn handle_idle_timeout(&mut self, cx: &mut crate::compositor::Context) -> EventResult { + pub fn handle_idle_timeout(&mut self, cx: &mut commands::Context) -> EventResult { if self.completion.is_some() || cx.editor.mode != Mode::Insert || !cx.editor.config().auto_completion @@ -1013,15 +1102,7 @@ impl EditorView { return EventResult::Ignored(None); } - let mut cx = commands::Context { - register: None, - editor: cx.editor, - jobs: cx.jobs, - count: None, - callback: None, - on_next_key_callback: None, - }; - crate::commands::insert::idle_completion(&mut cx); + crate::commands::insert::idle_completion(cx); EventResult::Consumed(None) } @@ -1308,6 +1389,11 @@ impl Component for EditorView { } self.on_next_key = cx.on_next_key_callback.take(); + match self.on_next_key { + Some(_) => self.pseudo_pending.push(key), + None => self.pseudo_pending.clear(), + } + // appease borrowck let callback = cx.callback.take(); @@ -1337,6 +1423,7 @@ impl Component for EditorView { } Event::Mouse(event) => self.handle_mouse_event(event, &mut cx), + Event::IdleTimeout => self.handle_idle_timeout(&mut cx), Event::FocusGained | Event::FocusLost => EventResult::Ignored(None), } } @@ -1408,8 +1495,8 @@ impl Component for EditorView { for key in self.keymaps.pending() { disp.push_str(&key.key_sequence_format()); } - if let Some(pseudo_pending) = &cx.editor.pseudo_pending { - disp.push_str(pseudo_pending.as_str()) + for key in &self.pseudo_pending { + disp.push_str(&key.key_sequence_format()); } let style = cx.editor.theme.get("ui.text"); let macro_width = if cx.editor.macro_recording.is_some() { diff --git a/helix-term/src/ui/fuzzy_match.rs b/helix-term/src/ui/fuzzy_match.rs new file mode 100644 index 00000000..e25d7328 --- /dev/null +++ b/helix-term/src/ui/fuzzy_match.rs @@ -0,0 +1,74 @@ +use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; +use fuzzy_matcher::FuzzyMatcher; + +#[cfg(test)] +mod test; + +pub struct FuzzyQuery { + queries: Vec<String>, +} + +impl FuzzyQuery { + pub fn new(query: &str) -> FuzzyQuery { + let mut saw_backslash = false; + let queries = query + .split(|c| { + saw_backslash = match c { + ' ' if !saw_backslash => return true, + '\\' => true, + _ => false, + }; + false + }) + .filter_map(|query| { + if query.is_empty() { + None + } else { + Some(query.replace("\\ ", " ")) + } + }) + .collect(); + FuzzyQuery { queries } + } + + pub fn fuzzy_match(&self, item: &str, matcher: &Matcher) -> Option<i64> { + // use the rank of the first query for the rank, because merging ranks is not really possible + // this behaviour matches fzf and skim + let score = matcher.fuzzy_match(item, self.queries.get(0)?)?; + if self + .queries + .iter() + .any(|query| matcher.fuzzy_match(item, query).is_none()) + { + return None; + } + Some(score) + } + + pub fn fuzzy_indicies(&self, item: &str, matcher: &Matcher) -> Option<(i64, Vec<usize>)> { + if self.queries.len() == 1 { + return matcher.fuzzy_indices(item, &self.queries[0]); + } + + // use the rank of the first query for the rank, because merging ranks is not really possible + // this behaviour matches fzf and skim + let (score, mut indicies) = matcher.fuzzy_indices(item, self.queries.get(0)?)?; + + // fast path for the common case of not using a space + // during matching this branch should be free thanks to branch prediction + if self.queries.len() == 1 { + return Some((score, indicies)); + } + + for query in &self.queries[1..] { + let (_, matched_indicies) = matcher.fuzzy_indices(item, query)?; + indicies.extend_from_slice(&matched_indicies); + } + + // deadup and remove duplicate matches + indicies.sort_unstable(); + indicies.dedup(); + + Some((score, indicies)) + } +} diff --git a/helix-term/src/ui/fuzzy_match/test.rs b/helix-term/src/ui/fuzzy_match/test.rs new file mode 100644 index 00000000..3f90ef68 --- /dev/null +++ b/helix-term/src/ui/fuzzy_match/test.rs @@ -0,0 +1,47 @@ +use crate::ui::fuzzy_match::FuzzyQuery; +use crate::ui::fuzzy_match::Matcher; + +fn run_test<'a>(query: &str, items: &'a [&'a str]) -> Vec<String> { + let query = FuzzyQuery::new(query); + let matcher = Matcher::default(); + items + .iter() + .filter_map(|item| { + let (_, indicies) = query.fuzzy_indicies(item, &matcher)?; + let matched_string = indicies + .iter() + .map(|&pos| item.chars().nth(pos).unwrap()) + .collect(); + Some(matched_string) + }) + .collect() +} + +#[test] +fn match_single_value() { + let matches = run_test("foo", &["foobar", "foo", "bar"]); + assert_eq!(matches, &["foo", "foo"]) +} + +#[test] +fn match_multiple_values() { + let matches = run_test( + "foo bar", + &["foo bar", "foo bar", "bar foo", "bar", "foo"], + ); + assert_eq!(matches, &["foobar", "foobar", "barfoo"]) +} + +#[test] +fn space_escape() { + let matches = run_test(r"foo\ bar", &["bar foo", "foo bar", "foobar"]); + assert_eq!(matches, &["foo bar"]) +} + +#[test] +fn trim() { + let matches = run_test(r" foo bar ", &["bar foo", "foo bar", "foobar"]); + assert_eq!(matches, &["barfoo", "foobar", "foobar"]); + let matches = run_test(r" foo bar\ ", &["bar foo", "foo bar", "foobar"]); + assert_eq!(matches, &["bar foo"]) +} diff --git a/helix-term/src/ui/lsp.rs b/helix-term/src/ui/lsp.rs index f2854551..393d24c4 100644 --- a/helix-term/src/ui/lsp.rs +++ b/helix-term/src/ui/lsp.rs @@ -68,8 +68,9 @@ impl Component for SignatureHelp { let (_, sig_text_height) = crate::ui::text::required_size(&sig_text, area.width); let sig_text_area = area.clip_top(1).with_height(sig_text_height); + let sig_text_area = sig_text_area.inner(&margin).intersection(surface.area); let sig_text_para = Paragraph::new(sig_text).wrap(Wrap { trim: false }); - sig_text_para.render(sig_text_area.inner(&margin), surface); + sig_text_para.render(sig_text_area, surface); if self.signature_doc.is_none() { return; diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index 1d247b1a..f77f5e80 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -105,7 +105,7 @@ impl<T: Item> Menu<T> { .iter() .enumerate() .filter_map(|(index, option)| { - let text: String = option.filter_text(&self.editor_data).into(); + let text = option.filter_text(&self.editor_data); // TODO: using fuzzy_indices could give us the char idx for match highlighting self.matcher .fuzzy_match(&text, pattern) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 60ad3b24..6ac4dbb7 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -1,5 +1,6 @@ mod completion; pub(crate) mod editor; +mod fuzzy_match; mod info; pub mod lsp; mod markdown; @@ -12,6 +13,8 @@ mod spinner; mod statusline; mod text; +use crate::compositor::{Component, Compositor}; +use crate::job; pub use completion::Completion; pub use editor::EditorView; pub use markdown::Markdown; @@ -24,7 +27,7 @@ pub use text::Text; use helix_core::regex::Regex; use helix_core::regex::RegexBuilder; -use helix_view::{Document, Editor, View}; +use helix_view::Editor; use std::path::PathBuf; @@ -59,7 +62,7 @@ pub fn regex_prompt( prompt: std::borrow::Cow<'static, str>, history_register: Option<char>, completion_fn: impl FnMut(&Editor, &str) -> Vec<prompt::Completion> + 'static, - fun: impl Fn(&mut View, &mut Document, Regex, PromptEvent) + 'static, + fun: impl Fn(&mut Editor, Regex, PromptEvent) + 'static, ) { let (view, doc) = current!(cx.editor); let doc_id = view.doc; @@ -106,11 +109,42 @@ pub fn regex_prompt( view.jumps.push((doc_id, snapshot.clone())); } - fun(view, doc, regex, event); + fun(cx.editor, regex, event); + let (view, doc) = current!(cx.editor); view.ensure_cursor_in_view(doc, config.scrolloff); } - Err(_err) => (), // TODO: mark command line as error + Err(err) => { + let (view, doc) = current!(cx.editor); + doc.set_selection(view.id, snapshot.clone()); + view.offset = offset_snapshot; + + if event == PromptEvent::Validate { + let callback = async move { + let call: job::Callback = Box::new( + move |_editor: &mut Editor, compositor: &mut Compositor| { + let contents = Text::new(format!("{}", err)); + let size = compositor.size(); + let mut popup = Popup::new("invalid-regex", contents) + .position(Some(helix_core::Position::new( + size.height as usize - 2, // 2 = statusline + commandline + 0, + ))) + .auto_close(true); + popup.required_size((size.width, size.height)); + + compositor.replace_or_push("invalid-regex", popup); + }, + ); + Ok(call) + }; + + cx.jobs.callback(callback); + } else { + // Update + // TODO: mark command line as error + } + } } } } diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index a56455d7..c7149c61 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -1,7 +1,7 @@ use crate::{ compositor::{Component, Compositor, Context, Event, EventResult}, ctrl, key, shift, - ui::{self, EditorView}, + ui::{self, fuzzy_match::FuzzyQuery, EditorView}, }; use tui::{ buffer::Buffer as Surface, @@ -9,7 +9,6 @@ use tui::{ }; use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; -use fuzzy_matcher::FuzzyMatcher; use tui::widgets::Widget; use std::time::Instant; @@ -161,6 +160,27 @@ impl<T: Item> FilePicker<T> { self.preview_cache.insert(path.to_owned(), preview); Preview::Cached(&self.preview_cache[path]) } + + fn handle_idle_timeout(&mut self, cx: &mut Context) -> EventResult { + // Try to find a document in the cache + let doc = self + .current_file(cx.editor) + .and_then(|(path, _range)| self.preview_cache.get_mut(&path)) + .and_then(|cache| match cache { + CachedPreview::Document(doc) => Some(doc), + _ => None, + }); + + // Then attempt to highlight it if it has no language set + if let Some(doc) = doc { + if doc.language_config().is_none() { + let loader = cx.editor.syn_loader.clone(); + doc.detect_language(loader); + } + } + + EventResult::Consumed(None) + } } impl<T: Item + 'static> Component for FilePicker<T> { @@ -261,6 +281,9 @@ impl<T: Item + 'static> Component for FilePicker<T> { } fn handle_event(&mut self, event: &Event, ctx: &mut Context) -> EventResult { + if let Event::IdleTimeout = event { + return self.handle_idle_timeout(ctx); + } // TODO: keybinds for scrolling preview self.picker.handle_event(event, ctx) } @@ -287,8 +310,6 @@ pub struct Picker<T: Item> { matcher: Box<Matcher>, /// (index, score) matches: Vec<(usize, i64)>, - /// Filter over original options. - filters: Vec<usize>, // could be optimized into bit but not worth it now /// Current height of the completions box completion_height: u16, @@ -323,7 +344,6 @@ impl<T: Item> Picker<T> { editor_data, matcher: Box::new(Matcher::default()), matches: Vec::new(), - filters: Vec::new(), cursor: 0, prompt, previous_pattern: String::new(), @@ -365,13 +385,14 @@ impl<T: Item> Picker<T> { .map(|(index, _option)| (index, 0)), ); } else if pattern.starts_with(&self.previous_pattern) { + let query = FuzzyQuery::new(pattern); // optimization: if the pattern is a more specific version of the previous one // then we can score the filtered set. self.matches.retain_mut(|(index, score)| { let option = &self.options[*index]; let text = option.sort_text(&self.editor_data); - match self.matcher.fuzzy_match(&text, pattern) { + match query.fuzzy_match(&text, &self.matcher) { Some(s) => { // Update the score *score = s; @@ -384,23 +405,17 @@ impl<T: Item> Picker<T> { self.matches .sort_unstable_by_key(|(_, score)| Reverse(*score)); } else { + let query = FuzzyQuery::new(pattern); self.matches.clear(); self.matches.extend( self.options .iter() .enumerate() .filter_map(|(index, option)| { - // filter options first before matching - if !self.filters.is_empty() { - // TODO: this filters functionality seems inefficient, - // instead store and operate on filters if any - self.filters.binary_search(&index).ok()?; - } - let text = option.filter_text(&self.editor_data); - self.matcher - .fuzzy_match(&text, pattern) + query + .fuzzy_match(&text, &self.matcher) .map(|score| (index, score)) }), ); @@ -460,14 +475,6 @@ impl<T: Item> Picker<T> { .map(|(index, _score)| &self.options[*index]) } - pub fn save_filter(&mut self, cx: &Context) { - self.filters.clear(); - self.filters - .extend(self.matches.iter().map(|(index, _)| *index)); - self.filters.sort_unstable(); // used for binary search later - self.prompt.clear(cx.editor); - } - pub fn toggle_preview(&mut self) { self.show_preview = !self.show_preview; } @@ -505,6 +512,9 @@ impl<T: Item + 'static> Component for Picker<T> { compositor.last_picker = compositor.pop(); }))); + // So that idle timeout retriggers + cx.editor.reset_idle_timer(); + match key_event { shift!(Tab) | key!(Up) | ctrl!('p') => { self.move_by(1, Direction::Backward); @@ -545,9 +555,6 @@ impl<T: Item + 'static> Component for Picker<T> { } return close_fn; } - ctrl!(' ') => { - self.save_filter(cx); - } ctrl!('t') => { self.toggle_preview(); } @@ -630,9 +637,8 @@ impl<T: Item + 'static> Component for Picker<T> { } let spans = option.label(&self.editor_data); - let (_score, highlights) = self - .matcher - .fuzzy_indices(&String::from(&spans), self.prompt.line()) + let (_score, highlights) = FuzzyQuery::new(self.prompt.line()) + .fuzzy_indicies(&String::from(&spans), &self.matcher) .unwrap_or_default(); spans.0.into_iter().fold(inner, |pos, span| { diff --git a/helix-term/src/ui/statusline.rs b/helix-term/src/ui/statusline.rs index 365e1ca9..b0e8ec5d 100644 --- a/helix-term/src/ui/statusline.rs +++ b/helix-term/src/ui/statusline.rs @@ -144,6 +144,7 @@ where helix_view::editor::StatusLineElement::Selections => render_selections, helix_view::editor::StatusLineElement::Position => render_position, helix_view::editor::StatusLineElement::PositionPercentage => render_position_percentage, + helix_view::editor::StatusLineElement::TotalLineNumbers => render_total_line_numbers, helix_view::editor::StatusLineElement::Separator => render_separator, helix_view::editor::StatusLineElement::Spacer => render_spacer, } @@ -154,16 +155,16 @@ where F: Fn(&mut RenderContext, String, Option<Style>) + Copy, { let visible = context.focused; - + let modenames = &context.editor.config().statusline.mode; write( context, format!( " {} ", if visible { match context.editor.mode() { - Mode::Insert => "INS", - Mode::Select => "SEL", - Mode::Normal => "NOR", + Mode::Insert => &modenames.insert, + Mode::Select => &modenames.select, + Mode::Normal => &modenames.normal, } } else { // If not focused, explicitly leave an empty space instead of returning None. @@ -276,6 +277,15 @@ where ); } +fn render_total_line_numbers<F>(context: &mut RenderContext, write: F) +where + F: Fn(&mut RenderContext, String, Option<Style>) + Copy, +{ + let total_line_numbers = context.doc.text().len_lines(); + + write(context, format!(" {} ", total_line_numbers), None); +} + fn render_position_percentage<F>(context: &mut RenderContext, write: F) where F: Fn(&mut RenderContext, String, Option<Style>) + Copy, |