aboutsummaryrefslogtreecommitdiff
path: root/helix-term/src/ui
diff options
context:
space:
mode:
Diffstat (limited to 'helix-term/src/ui')
-rw-r--r--helix-term/src/ui/completion.rs12
-rw-r--r--helix-term/src/ui/editor.rs191
-rw-r--r--helix-term/src/ui/fuzzy_match.rs74
-rw-r--r--helix-term/src/ui/fuzzy_match/test.rs47
-rw-r--r--helix-term/src/ui/lsp.rs3
-rw-r--r--helix-term/src/ui/menu.rs2
-rw-r--r--helix-term/src/ui/mod.rs42
-rw-r--r--helix-term/src/ui/picker.rs64
-rw-r--r--helix-term/src/ui/statusline.rs18
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,