aboutsummaryrefslogtreecommitdiff
path: root/helix-term/src/ui/editor.rs
diff options
context:
space:
mode:
Diffstat (limited to 'helix-term/src/ui/editor.rs')
-rw-r--r--helix-term/src/ui/editor.rs191
1 files changed, 139 insertions, 52 deletions
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() {