Extract gutters into helix-view

This commit is contained in:
Blaž Hrastnik 2021-11-23 12:56:46 +09:00
parent 27c1a84f05
commit 225e8ccf31
4 changed files with 117 additions and 100 deletions

View file

@ -17,7 +17,6 @@ use helix_core::{
}; };
use helix_view::{ use helix_view::{
document::{Mode, SCRATCH_BUFFER_NAME}, document::{Mode, SCRATCH_BUFFER_NAME},
editor::{Config, LineNumber},
graphics::{CursorKind, Modifier, Rect, Style}, graphics::{CursorKind, Modifier, Rect, Style},
info::Info, info::Info,
input::KeyEvent, input::KeyEvent,
@ -421,97 +420,12 @@ impl EditorView {
.map(|range| range.cursor_line(text)) .map(|range| range.cursor_line(text))
.collect(); .collect();
use std::fmt::Write;
fn diagnostic<'doc>(
doc: &'doc Document,
_view: &View,
theme: &Theme,
_config: &Config,
_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, out: &mut String| {
use helix_core::diagnostic::Severity;
if let Some(diagnostic) = diagnostics.iter().find(|d| d.line == line) {
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
})
}
fn line_number<'doc>(
doc: &'doc Document,
view: &View,
theme: &Theme,
config: &Config,
is_focused: bool,
width: usize,
) -> GutterFn<'doc> {
let text = doc.text().slice(..);
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) < text.len_bytes();
let linenr = theme.get("ui.linenr");
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 config = config.line_number;
Box::new(move |line: usize, selected: bool, out: &mut String| {
if line == last_line && !draw_last {
write!(out, "{:>1$}", '~', width).unwrap();
Some(linenr)
} else {
let line = match config {
LineNumber::Absolute => line + 1,
LineNumber::Relative => {
if current_line == line {
line + 1
} else {
abs_diff(current_line, line)
}
}
};
let style = if selected && is_focused {
linenr_select
} else {
linenr
};
write!(out, "{:>1$}", line, width).unwrap();
Some(style)
}
})
}
type GutterFn<'doc> = Box<dyn Fn(usize, bool, &mut String) -> Option<Style> + 'doc>;
type Gutter =
for<'doc> fn(&'doc Document, &View, &Theme, &Config, bool, usize) -> GutterFn<'doc>;
let gutters: &[(Gutter, usize)] = &[(diagnostic, 1), (line_number, 5)];
let mut offset = 0; let mut offset = 0;
// avoid lots of small allocations by reusing a text buffer for each line // avoid lots of small allocations by reusing a text buffer for each line
let mut text = String::with_capacity(8); let mut text = String::with_capacity(8);
for (constructor, width) in gutters { for (constructor, width) in view.gutters() {
let gutter = constructor(doc, view, theme, config, is_focused, *width); let gutter = constructor(doc, view, theme, config, is_focused, *width);
text.reserve(*width); // ensure there's enough space for the gutter text.reserve(*width); // ensure there's enough space for the gutter
for (i, line) in (view.offset.row..(last_line + 1)).enumerate() { for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
@ -1214,12 +1128,3 @@ fn canonicalize_key(key: &mut KeyEvent) {
key.modifiers.remove(KeyModifiers::SHIFT) key.modifiers.remove(KeyModifiers::SHIFT)
} }
} }
#[inline]
const fn abs_diff(a: usize, b: usize) -> usize {
if a > b {
a - b
} else {
b - a
}
}

95
helix-view/src/gutter.rs Normal file
View file

@ -0,0 +1,95 @@
use std::fmt::Write;
use crate::{editor::Config, graphics::Style, Document, Theme, View};
pub type GutterFn<'doc> = Box<dyn Fn(usize, bool, &mut String) -> Option<Style> + 'doc>;
pub type Gutter =
for<'doc> fn(&'doc Document, &View, &Theme, &Config, bool, usize) -> GutterFn<'doc>;
pub fn diagnostic<'doc>(
doc: &'doc Document,
_view: &View,
theme: &Theme,
_config: &Config,
_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, out: &mut String| {
use helix_core::diagnostic::Severity;
if let Some(diagnostic) = diagnostics.iter().find(|d| d.line == line) {
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_number<'doc>(
doc: &'doc Document,
view: &View,
theme: &Theme,
config: &Config,
is_focused: bool,
width: usize,
) -> GutterFn<'doc> {
let text = doc.text().slice(..);
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) < text.len_bytes();
let linenr = theme.get("ui.linenr");
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 config = config.line_number;
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::editor::LineNumber;
let line = match config {
LineNumber::Absolute => line + 1,
LineNumber::Relative => {
if current_line == line {
line + 1
} else {
abs_diff(current_line, line)
}
}
};
let style = if selected && is_focused {
linenr_select
} else {
linenr
};
write!(out, "{:>1$}", line, width).unwrap();
Some(style)
}
})
}
#[inline(always)]
const fn abs_diff(a: usize, b: usize) -> usize {
if a > b {
a - b
} else {
b - a
}
}

View file

@ -5,6 +5,7 @@ pub mod clipboard;
pub mod document; pub mod document;
pub mod editor; pub mod editor;
pub mod graphics; pub mod graphics;
pub mod gutter;
pub mod info; pub mod info;
pub mod input; pub mod input;
pub mod keyboard; pub mod keyboard;

View file

@ -1,6 +1,10 @@
use std::borrow::Cow; use std::borrow::Cow;
use crate::{graphics::Rect, Document, DocumentId, ViewId}; use crate::{
graphics::Rect,
gutter::{self, Gutter},
Document, DocumentId, ViewId,
};
use helix_core::{ use helix_core::{
graphemes::{grapheme_width, RopeGraphemes}, graphemes::{grapheme_width, RopeGraphemes},
line_ending::line_end_char_index, line_ending::line_end_char_index,
@ -60,6 +64,8 @@ impl JumpList {
} }
} }
const GUTTERS: &[(Gutter, usize)] = &[(gutter::diagnostic, 1), (gutter::line_number, 5)];
#[derive(Debug)] #[derive(Debug)]
pub struct View { pub struct View {
pub id: ViewId, pub id: ViewId,
@ -83,10 +89,19 @@ impl View {
} }
} }
pub fn gutters(&self) -> &[(Gutter, usize)] {
GUTTERS
}
pub fn inner_area(&self) -> Rect { pub fn inner_area(&self) -> Rect {
// TODO: not ideal // TODO: cache this
const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter let offset = self
self.area.clip_left(OFFSET).clip_bottom(1) // -1 for statusline .gutters()
.iter()
.map(|(_, width)| *width as u16)
.sum::<u16>()
+ 1; // +1 for some space between gutters and line
self.area.clip_left(offset).clip_bottom(1) // -1 for statusline
} }
// //
@ -276,6 +291,7 @@ mod tests {
use super::*; use super::*;
use helix_core::Rope; use helix_core::Rope;
const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
// const OFFSET: u16 = GUTTERS.iter().map(|(_, width)| *width as u16).sum();
#[test] #[test]
fn test_text_pos_at_screen_coords() { fn test_text_pos_at_screen_coords() {