Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-view/src/gutter.rs')
-rw-r--r--helix-view/src/gutter.rs448
1 files changed, 93 insertions, 355 deletions
diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs
index 7506e515..6a77c41f 100644
--- a/helix-view/src/gutter.rs
+++ b/helix-view/src/gutter.rs
@@ -1,230 +1,103 @@
use std::fmt::Write;
-use helix_core::syntax::config::LanguageServerFeature;
-
use crate::{
- editor::GutterType,
- graphics::{Style, UnderlineStyle},
+ graphics::{Color, Modifier, Style},
Document, Editor, Theme, View,
};
-fn count_digits(n: usize) -> usize {
- (usize::checked_ilog10(n).unwrap_or(0) + 1) as usize
-}
-
-pub type GutterFn<'doc> = Box<dyn FnMut(usize, bool, bool, &mut String) -> Option<Style> + 'doc>;
+pub type GutterFn<'doc> = Box<dyn Fn(usize, bool, &mut String) -> Option<Style> + 'doc>;
pub type Gutter =
for<'doc> fn(&'doc Editor, &'doc Document, &View, &Theme, bool, usize) -> GutterFn<'doc>;
-impl GutterType {
- pub fn style<'doc>(
- self,
- editor: &'doc Editor,
- doc: &'doc Document,
- view: &View,
- theme: &Theme,
- is_focused: bool,
- ) -> GutterFn<'doc> {
- match self {
- GutterType::Diagnostics => {
- diagnostics_or_breakpoints(editor, doc, view, theme, is_focused)
- }
- GutterType::LineNumbers => line_numbers(editor, doc, view, theme, is_focused),
- GutterType::Spacer => padding(editor, doc, view, theme, is_focused),
- GutterType::Diff => diff(editor, doc, view, theme, is_focused),
- }
- }
-
- pub fn width(self, view: &View, doc: &Document) -> usize {
- match self {
- GutterType::Diagnostics => 1,
- GutterType::LineNumbers => line_numbers_width(view, doc),
- GutterType::Spacer => 1,
- GutterType::Diff => 1,
- }
- }
-}
-
pub fn diagnostic<'doc>(
_editor: &'doc Editor,
doc: &'doc Document,
_view: &View,
theme: &Theme,
_is_focused: bool,
+ _width: usize,
) -> GutterFn<'doc> {
let warning = theme.get("warning");
let error = theme.get("error");
let info = theme.get("info");
let hint = theme.get("hint");
- let diagnostics = &doc.diagnostics;
-
- Box::new(
- move |line: usize, _selected: bool, first_visual_line: bool, out: &mut String| {
- if !first_visual_line {
- return None;
- }
- use helix_core::diagnostic::Severity;
- let first_diag_idx_maybe_on_line = diagnostics.partition_point(|d| d.line < line);
- let diagnostics_on_line = diagnostics[first_diag_idx_maybe_on_line..]
- .iter()
- .take_while(|d| {
- d.line == line
- && d.provider.language_server_id().is_none_or(|id| {
- doc.language_servers_with_feature(LanguageServerFeature::Diagnostics)
- .any(|ls| ls.id() == id)
- })
- });
- diagnostics_on_line.max_by_key(|d| d.severity).map(|d| {
- write!(out, "●").ok();
- match d.severity {
- Some(Severity::Error) => error,
- Some(Severity::Warning) | None => warning,
- Some(Severity::Info) => info,
- Some(Severity::Hint) => hint,
- }
- })
- },
- )
-}
-
-pub fn diff<'doc>(
- _editor: &'doc Editor,
- doc: &'doc Document,
- _view: &View,
- theme: &Theme,
- _is_focused: bool,
-) -> GutterFn<'doc> {
- let added = theme.get("diff.plus.gutter");
- let deleted = theme.get("diff.minus.gutter");
- let modified = theme.get("diff.delta.gutter");
- if let Some(diff_handle) = doc.diff_handle() {
- let hunks = diff_handle.load();
- let mut hunk_i = 0;
- let mut hunk = hunks.nth_hunk(hunk_i);
- Box::new(
- move |line: usize, _selected: bool, first_visual_line: bool, out: &mut String| {
- // truncating the line is fine here because we don't compute diffs
- // for files with more lines than i32::MAX anyways
- // we need to special case removals here
- // these technically do not have a range of lines to highlight (`hunk.after.start == hunk.after.end`).
- // However we still want to display these hunks correctly we must not yet skip to the next hunk here
- while hunk.after.end < line as u32
- || !hunk.is_pure_removal() && line as u32 == hunk.after.end
- {
- hunk_i += 1;
- hunk = hunks.nth_hunk(hunk_i);
- }
-
- if hunk.after.start > line as u32 {
- return None;
- }
-
- let (icon, style) = if hunk.is_pure_insertion() {
- ("▍", added)
- } else if hunk.is_pure_removal() {
- if !first_visual_line {
- return None;
- }
- ("▔", deleted)
- } else {
- ("▍", modified)
- };
-
- write!(out, "{}", icon).unwrap();
- Some(style)
- },
- )
- } else {
- Box::new(move |_, _, _, _| None)
- }
+ let diagnostics = doc.diagnostics();
+
+ Box::new(move |line: usize, _selected: bool, out: &mut String| {
+ use helix_core::diagnostic::Severity;
+ if let Ok(index) = diagnostics.binary_search_by_key(&line, |d| d.line) {
+ let diagnostic = &diagnostics[index];
+ write!(out, "●").unwrap();
+ return Some(match diagnostic.severity {
+ Some(Severity::Error) => error,
+ Some(Severity::Warning) | None => warning,
+ Some(Severity::Info) => info,
+ Some(Severity::Hint) => hint,
+ });
+ }
+ None
+ })
}
-pub fn line_numbers<'doc>(
+pub fn line_number<'doc>(
editor: &'doc Editor,
doc: &'doc Document,
view: &View,
theme: &Theme,
is_focused: bool,
+ width: usize,
) -> GutterFn<'doc> {
let text = doc.text().slice(..);
- let width = line_numbers_width(view, doc);
-
- let last_line_in_view = view.estimate_last_doc_line(doc);
-
+ let last_line = view.last_line(doc);
// Whether to draw the line number for the last line of the
// document or not. We only draw it if it's not an empty line.
- let draw_last = text.line_to_byte(last_line_in_view) < text.len_bytes();
+ let draw_last = text.line_to_byte(last_line) < text.len_bytes();
let linenr = theme.get("ui.linenr");
- let linenr_select = theme.get("ui.linenr.selected");
+ let linenr_select: Style = theme.try_get("ui.linenr.selected").unwrap_or(linenr);
let current_line = doc
.text()
.char_to_line(doc.selection(view.id).primary().cursor(text));
- let line_number = editor.config().line_number;
- let mode = editor.mode;
-
- Box::new(
- move |line: usize, selected: bool, first_visual_line: bool, out: &mut String| {
- if line == last_line_in_view && !draw_last {
- write!(out, "{:>1$}", '~', width).unwrap();
- Some(linenr)
- } else {
- use crate::{document::Mode, editor::LineNumber};
-
- let relative = line_number == LineNumber::Relative
- && mode != Mode::Insert
- && is_focused
- && current_line != line;
-
- let display_num = if relative {
- current_line.abs_diff(line)
- } else {
- line + 1
- };
+ let config = editor.config.line_number;
+ let mode = doc.mode;
- let style = if selected && is_focused {
- linenr_select
- } else {
- linenr
- };
+ Box::new(move |line: usize, selected: bool, out: &mut String| {
+ if line == last_line && !draw_last {
+ write!(out, "{:>1$}", '~', width).unwrap();
+ Some(linenr)
+ } else {
+ use crate::{document::Mode, editor::LineNumber};
- if first_visual_line {
- write!(out, "{:>1$}", display_num, width).unwrap();
- } else {
- write!(out, "{:>1$}", " ", width).unwrap();
- }
+ let relative = config == LineNumber::Relative
+ && mode != Mode::Insert
+ && is_focused
+ && current_line != line;
- first_visual_line.then_some(style)
- }
- },
- )
-}
-
-/// The width of a "line-numbers" gutter
-///
-/// The width of the gutter depends on the number of lines in the document,
-/// whether there is content on the last line (the `~` line), and the
-/// `editor.gutters.line-numbers.min-width` settings.
-fn line_numbers_width(view: &View, doc: &Document) -> usize {
- let text = doc.text();
- let last_line = text.len_lines().saturating_sub(1);
- let draw_last = text.line_to_byte(last_line) < text.len_bytes();
- let last_drawn = if draw_last { last_line + 1 } else { last_line };
- let digits = count_digits(last_drawn);
- let n_min = view.gutters.line_numbers.min_width;
- digits.max(n_min)
+ let display_num = if relative {
+ abs_diff(current_line, line)
+ } else {
+ line + 1
+ };
+ let style = if selected && is_focused {
+ linenr_select
+ } else {
+ linenr
+ };
+ write!(out, "{:>1$}", display_num, width).unwrap();
+ Some(style)
+ }
+ })
}
-pub fn padding<'doc>(
- _editor: &'doc Editor,
- _doc: &'doc Document,
- _view: &View,
- _theme: &Theme,
- _is_focused: bool,
-) -> GutterFn<'doc> {
- Box::new(|_line: usize, _selected: bool, _first_visual_line: bool, _out: &mut String| None)
+#[inline(always)]
+const fn abs_diff(a: usize, b: usize) -> usize {
+ if a > b {
+ a - b
+ } else {
+ b - a
+ }
}
pub fn breakpoints<'doc>(
@@ -233,77 +106,51 @@ pub fn breakpoints<'doc>(
_view: &View,
theme: &Theme,
_is_focused: bool,
+ _width: usize,
) -> GutterFn<'doc> {
+ let warning = theme.get("warning");
let error = theme.get("error");
let info = theme.get("info");
- let breakpoint_style = theme.get("ui.debug.breakpoint");
let breakpoints = doc.path().and_then(|path| editor.breakpoints.get(path));
let breakpoints = match breakpoints {
Some(breakpoints) => breakpoints,
- None => return Box::new(move |_, _, _, _| None),
+ None => return Box::new(move |_, _, _| None),
};
- Box::new(
- move |line: usize, _selected: bool, first_visual_line: bool, out: &mut String| {
- if !first_visual_line {
- return None;
- }
- let breakpoint = breakpoints
- .iter()
- .find(|breakpoint| breakpoint.line == line)?;
+ Box::new(move |line: usize, _selected: bool, out: &mut String| {
+ let breakpoint = breakpoints
+ .iter()
+ .find(|breakpoint| breakpoint.line == line)?;
+
+ let mut style = if breakpoint.condition.is_some() && breakpoint.log_message.is_some() {
+ error.add_modifier(Modifier::UNDERLINED)
+ } else if breakpoint.condition.is_some() {
+ error
+ } else if breakpoint.log_message.is_some() {
+ info
+ } else {
+ warning
+ };
- let style = if breakpoint.condition.is_some() && breakpoint.log_message.is_some() {
- error.underline_style(UnderlineStyle::Line)
- } else if breakpoint.condition.is_some() {
- error
- } else if breakpoint.log_message.is_some() {
- info
+ if !breakpoint.verified {
+ // Faded colors
+ style = if let Some(Color::Rgb(r, g, b)) = style.fg {
+ style.fg(Color::Rgb(
+ ((r as f32) * 0.4).floor() as u8,
+ ((g as f32) * 0.4).floor() as u8,
+ ((b as f32) * 0.4).floor() as u8,
+ ))
} else {
- breakpoint_style
- };
-
- let sym = if breakpoint.verified { "●" } else { "◯" };
- write!(out, "{}", sym).unwrap();
- Some(style)
- },
- )
-}
-
-fn execution_pause_indicator<'doc>(
- editor: &'doc Editor,
- doc: &'doc Document,
- theme: &Theme,
- is_focused: bool,
-) -> GutterFn<'doc> {
- let style = theme.get("ui.debug.active");
- let current_stack_frame = editor.current_stack_frame();
- let frame_line = current_stack_frame.map(|frame| frame.line - 1);
- let frame_source_path = current_stack_frame.map(|frame| {
- frame
- .source
- .as_ref()
- .and_then(|source| source.path.as_ref())
- });
- let should_display_for_current_doc =
- doc.path().is_some() && frame_source_path.unwrap_or(None) == doc.path();
-
- Box::new(
- move |line: usize, _selected: bool, first_visual_line: bool, out: &mut String| {
- if !first_visual_line
- || !is_focused
- || line != frame_line?
- || !should_display_for_current_doc
- {
- return None;
+ style.fg(Color::Gray)
}
+ };
- let sym = "▶";
- write!(out, "{}", sym).unwrap();
- Some(style)
- },
- )
+ let sym = if breakpoint.verified { "▲" } else { "⊚" };
+ write!(out, "{}", sym).unwrap();
+ Some(style)
+ })
}
pub fn diagnostics_or_breakpoints<'doc>(
@@ -312,121 +159,12 @@ pub fn diagnostics_or_breakpoints<'doc>(
view: &View,
theme: &Theme,
is_focused: bool,
+ width: usize,
) -> GutterFn<'doc> {
- let mut diagnostics = diagnostic(editor, doc, view, theme, is_focused);
- let mut breakpoints = breakpoints(editor, doc, view, theme, is_focused);
- let mut execution_pause_indicator = execution_pause_indicator(editor, doc, theme, is_focused);
+ let diagnostics = diagnostic(editor, doc, view, theme, is_focused, width);
+ let breakpoints = breakpoints(editor, doc, view, theme, is_focused, width);
- Box::new(move |line, selected, first_visual_line: bool, out| {
- execution_pause_indicator(line, selected, first_visual_line, out)
- .or_else(|| breakpoints(line, selected, first_visual_line, out))
- .or_else(|| diagnostics(line, selected, first_visual_line, out))
+ Box::new(move |line, selected, out| {
+ breakpoints(line, selected, out).or_else(|| diagnostics(line, selected, out))
})
}
-
-#[cfg(test)]
-mod tests {
- use std::sync::Arc;
-
- use super::*;
- use crate::document::Document;
- use crate::editor::{Config, GutterConfig, GutterLineNumbersConfig};
- use crate::graphics::Rect;
- use crate::DocumentId;
- use arc_swap::ArcSwap;
- use helix_core::{syntax, Rope};
-
- #[test]
- fn test_default_gutter_widths() {
- let mut view = View::new(DocumentId::default(), GutterConfig::default());
- view.area = Rect::new(40, 40, 40, 40);
-
- let rope = Rope::from_str("abc\n\tdef");
- let doc = Document::from(
- rope,
- None,
- Arc::new(ArcSwap::new(Arc::new(Config::default()))),
- Arc::new(ArcSwap::from_pointee(syntax::Loader::default())),
- );
-
- assert_eq!(view.gutters.layout.len(), 5);
- assert_eq!(view.gutters.layout[0].width(&view, &doc), 1);
- assert_eq!(view.gutters.layout[1].width(&view, &doc), 1);
- assert_eq!(view.gutters.layout[2].width(&view, &doc), 3);
- assert_eq!(view.gutters.layout[3].width(&view, &doc), 1);
- assert_eq!(view.gutters.layout[4].width(&view, &doc), 1);
- }
-
- #[test]
- fn test_configured_gutter_widths() {
- let gutters = GutterConfig {
- layout: vec![GutterType::Diagnostics],
- ..Default::default()
- };
-
- let mut view = View::new(DocumentId::default(), gutters);
- view.area = Rect::new(40, 40, 40, 40);
-
- let rope = Rope::from_str("abc\n\tdef");
- let doc = Document::from(
- rope,
- None,
- Arc::new(ArcSwap::new(Arc::new(Config::default()))),
- Arc::new(ArcSwap::from_pointee(syntax::Loader::default())),
- );
-
- assert_eq!(view.gutters.layout.len(), 1);
- assert_eq!(view.gutters.layout[0].width(&view, &doc), 1);
-
- let gutters = GutterConfig {
- layout: vec![GutterType::Diagnostics, GutterType::LineNumbers],
- line_numbers: GutterLineNumbersConfig { min_width: 10 },
- };
-
- let mut view = View::new(DocumentId::default(), gutters);
- view.area = Rect::new(40, 40, 40, 40);
-
- let rope = Rope::from_str("abc\n\tdef");
- let doc = Document::from(
- rope,
- None,
- Arc::new(ArcSwap::new(Arc::new(Config::default()))),
- Arc::new(ArcSwap::from_pointee(syntax::Loader::default())),
- );
-
- assert_eq!(view.gutters.layout.len(), 2);
- assert_eq!(view.gutters.layout[0].width(&view, &doc), 1);
- assert_eq!(view.gutters.layout[1].width(&view, &doc), 10);
- }
-
- #[test]
- fn test_line_numbers_gutter_width_resizes() {
- let gutters = GutterConfig {
- layout: vec![GutterType::Diagnostics, GutterType::LineNumbers],
- line_numbers: GutterLineNumbersConfig { min_width: 1 },
- };
-
- let mut view = View::new(DocumentId::default(), gutters);
- view.area = Rect::new(40, 40, 40, 40);
-
- let rope = Rope::from_str("a\nb");
- let doc_short = Document::from(
- rope,
- None,
- Arc::new(ArcSwap::new(Arc::new(Config::default()))),
- Arc::new(ArcSwap::from_pointee(syntax::Loader::default())),
- );
-
- let rope = Rope::from_str("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk\nl\nm\nn\no\np");
- let doc_long = Document::from(
- rope,
- None,
- Arc::new(ArcSwap::new(Arc::new(Config::default()))),
- Arc::new(ArcSwap::from_pointee(syntax::Loader::default())),
- );
-
- assert_eq!(view.gutters.layout.len(), 2);
- assert_eq!(view.gutters.layout[1].width(&view, &doc_short), 1);
- assert_eq!(view.gutters.layout[1].width(&view, &doc_long), 2);
- }
-}