aboutsummaryrefslogtreecommitdiff
path: root/helix-view
diff options
context:
space:
mode:
authorDoug Kelkhoff2022-11-08 12:19:59 +0000
committerGitHub2022-11-08 12:19:59 +0000
commit7ed9e9cf2567ee5e23cd8694ffccb4b38602c02a (patch)
treed815469c54dc42dc66e57fa85e389325e8b7a3a6 /helix-view
parentc94feed83d746e71fb030639d740af85162b0763 (diff)
Dynamically resize line number gutter width (#3469)
* dynamically resize line number gutter width * removing digits lower-bound, permitting spacer * removing max line num char limit; adding notes; qualified successors; notes * updating tests to use new line number width when testing views * linenr width based on document line count * using min width of 2 so line numbers relative is useful * lint rolling; removing unnecessary type parameter lifetime * merge change resolution * reformat code * rename row_styler to style; add int_log resource * adding spacer to gutters default; updating book config entry * adding view.inner_height(), swap for loop for iterator * reverting change of current! to view! now that doc is not needed
Diffstat (limited to 'helix-view')
-rw-r--r--helix-view/src/editor.rs9
-rw-r--r--helix-view/src/gutter.rs57
-rw-r--r--helix-view/src/lib.rs2
-rw-r--r--helix-view/src/tree.rs4
-rw-r--r--helix-view/src/view.rs145
5 files changed, 125 insertions, 92 deletions
diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs
index bcd8dedb..db97cbb1 100644
--- a/helix-view/src/editor.rs
+++ b/helix-view/src/editor.rs
@@ -456,6 +456,7 @@ impl std::str::FromStr for GutterType {
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"diagnostics" => Ok(Self::Diagnostics),
+ "spacer" => Ok(Self::Spacer),
"line-numbers" => Ok(Self::LineNumbers),
_ => anyhow::bail!("Gutter type can only be `diagnostics` or `line-numbers`."),
}
@@ -589,7 +590,11 @@ impl Default for Config {
line_number: LineNumber::Absolute,
cursorline: false,
cursorcolumn: false,
- gutters: vec![GutterType::Diagnostics, GutterType::LineNumbers],
+ gutters: vec![
+ GutterType::Diagnostics,
+ GutterType::Spacer,
+ GutterType::LineNumbers,
+ ],
middle_click_paste: true,
auto_pairs: AutoPairConfig::default(),
auto_completion: true,
@@ -1308,7 +1313,7 @@ impl Editor {
.primary()
.cursor(doc.text().slice(..));
if let Some(mut pos) = view.screen_coords_at_pos(doc, doc.text().slice(..), cursor) {
- let inner = view.inner_area();
+ let inner = view.inner_area(doc);
pos.col += inner.x as usize;
pos.row += inner.y as usize;
let cursorkind = config.cursor_shape.from_mode(self.mode);
diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs
index 2c207d27..61a17791 100644
--- a/helix-view/src/gutter.rs
+++ b/helix-view/src/gutter.rs
@@ -1,21 +1,54 @@
use std::fmt::Write;
use crate::{
+ editor::GutterType,
graphics::{Color, Style, UnderlineStyle},
Document, Editor, Theme, View,
};
+fn count_digits(n: usize) -> usize {
+ // NOTE: if int_log gets standardized in stdlib, can use checked_log10
+ // (https://github.com/rust-lang/rust/issues/70887#issue)
+ std::iter::successors(Some(n), |&n| (n >= 10).then(|| n / 10)).count()
+}
+
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),
+ }
+ }
+
+ pub fn width(self, _view: &View, doc: &Document) -> usize {
+ match self {
+ GutterType::Diagnostics => 1,
+ GutterType::LineNumbers => line_numbers_width(_view, doc),
+ GutterType::Spacer => 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");
@@ -56,10 +89,11 @@ pub fn line_numbers<'doc>(
view: &View,
theme: &Theme,
is_focused: bool,
- width: usize,
) -> GutterFn<'doc> {
let text = doc.text().slice(..);
let last_line = view.last_line(doc);
+ let width = GutterType::LineNumbers.width(view, 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();
@@ -91,24 +125,35 @@ pub fn line_numbers<'doc>(
} else {
line + 1
};
+
let style = if selected && is_focused {
linenr_select
} else {
linenr
};
+
write!(out, "{:>1$}", display_num, width).unwrap();
Some(style)
}
})
}
+pub 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 };
+
+ // set a lower bound to 2-chars to minimize ambiguous relative line numbers
+ std::cmp::max(count_digits(last_drawn), 2)
+}
+
pub fn padding<'doc>(
_editor: &'doc Editor,
_doc: &'doc Document,
_view: &View,
_theme: &Theme,
_is_focused: bool,
- _width: usize,
) -> GutterFn<'doc> {
Box::new(|_line: usize, _selected: bool, _out: &mut String| None)
}
@@ -128,7 +173,6 @@ 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");
@@ -181,10 +225,9 @@ pub fn diagnostics_or_breakpoints<'doc>(
view: &View,
theme: &Theme,
is_focused: bool,
- width: usize,
) -> GutterFn<'doc> {
- let diagnostics = diagnostic(editor, doc, view, theme, is_focused, width);
- let breakpoints = breakpoints(editor, doc, view, theme, is_focused, width);
+ let diagnostics = diagnostic(editor, doc, view, theme, is_focused);
+ let breakpoints = breakpoints(editor, doc, view, theme, is_focused);
Box::new(move |line, selected, out| {
breakpoints(line, selected, out).or_else(|| diagnostics(line, selected, out))
diff --git a/helix-view/src/lib.rs b/helix-view/src/lib.rs
index 52044ac7..4c32b356 100644
--- a/helix-view/src/lib.rs
+++ b/helix-view/src/lib.rs
@@ -55,7 +55,7 @@ pub fn align_view(doc: &Document, view: &mut View, align: Align) {
.cursor(doc.text().slice(..));
let line = doc.text().char_to_line(pos);
- let last_line_height = view.inner_area().height.saturating_sub(1) as usize;
+ let last_line_height = view.inner_height().saturating_sub(1);
let relative = match align {
Align::Center => last_line_height / 2,
diff --git a/helix-view/src/tree.rs b/helix-view/src/tree.rs
index a1977764..6174021c 100644
--- a/helix-view/src/tree.rs
+++ b/helix-view/src/tree.rs
@@ -499,7 +499,7 @@ impl Tree {
// in a vertical container (and already correct based on previous search)
child_id = *container.children.iter().min_by_key(|id| {
let x = match &self.nodes[**id].content {
- Content::View(view) => view.inner_area().left(),
+ Content::View(view) => view.area.left(),
Content::Container(container) => container.area.left(),
};
(current_x as i16 - x as i16).abs()
@@ -510,7 +510,7 @@ impl Tree {
// in a horizontal container (and already correct based on previous search)
child_id = *container.children.iter().min_by_key(|id| {
let y = match &self.nodes[**id].content {
- Content::View(view) => view.inner_area().top(),
+ Content::View(view) => view.area.top(),
Content::Container(container) => container.area.top(),
};
(current_y as i16 - y as i16).abs()
diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs
index 62984b88..6da4df1f 100644
--- a/helix-view/src/view.rs
+++ b/helix-view/src/view.rs
@@ -1,8 +1,4 @@
-use crate::{
- graphics::Rect,
- gutter::{self, Gutter},
- Document, DocumentId, ViewId,
-};
+use crate::{editor::GutterType, graphics::Rect, Document, DocumentId, ViewId};
use helix_core::{
pos_at_visual_coords, visual_coords_at_pos, Position, RopeSlice, Selection, Transaction,
};
@@ -98,11 +94,8 @@ pub struct View {
pub last_modified_docs: [Option<DocumentId>; 2],
/// used to store previous selections of tree-sitter objects
pub object_selections: Vec<Selection>,
- /// Gutter (constructor) and width of gutter, used to calculate
- /// `gutter_offset`
- gutters: Vec<(Gutter, usize)>,
- /// cached total width of gutter
- gutter_offset: u16,
+ /// GutterTypes used to fetch Gutter (constructor) and width for rendering
+ gutters: Vec<GutterType>,
}
impl fmt::Debug for View {
@@ -117,28 +110,6 @@ impl fmt::Debug for View {
impl View {
pub fn new(doc: DocumentId, gutter_types: Vec<crate::editor::GutterType>) -> Self {
- let mut gutters: Vec<(Gutter, usize)> = vec![];
- let mut gutter_offset = 0;
- use crate::editor::GutterType;
- for gutter_type in &gutter_types {
- let width = match gutter_type {
- GutterType::Diagnostics => 1,
- GutterType::LineNumbers => 5,
- GutterType::Spacer => 1,
- };
- gutter_offset += width;
- gutters.push((
- match gutter_type {
- GutterType::Diagnostics => gutter::diagnostics_or_breakpoints,
- GutterType::LineNumbers => gutter::line_numbers,
- GutterType::Spacer => gutter::padding,
- },
- width as usize,
- ));
- }
- if !gutter_types.is_empty() {
- gutter_offset += 1;
- }
Self {
id: ViewId::default(),
doc,
@@ -148,8 +119,7 @@ impl View {
docs_access_history: Vec::new(),
last_modified_docs: [None, None],
object_selections: Vec::new(),
- gutters,
- gutter_offset,
+ gutters: gutter_types,
}
}
@@ -160,15 +130,32 @@ impl View {
self.docs_access_history.push(id);
}
- pub fn inner_area(&self) -> Rect {
- // TODO add abilty to not use cached offset for runtime configurable gutter
- self.area.clip_left(self.gutter_offset).clip_bottom(1) // -1 for statusline
+ pub fn inner_area(&self, doc: &Document) -> Rect {
+ self.area.clip_left(self.gutter_offset(doc)).clip_bottom(1) // -1 for statusline
+ }
+
+ pub fn inner_height(&self) -> usize {
+ self.area.clip_bottom(1).height.into() // -1 for statusline
}
- pub fn gutters(&self) -> &[(Gutter, usize)] {
+ pub fn gutters(&self) -> &[GutterType] {
&self.gutters
}
+ pub fn gutter_offset(&self, doc: &Document) -> u16 {
+ let mut offset = self
+ .gutters
+ .iter()
+ .map(|gutter| gutter.width(self, doc) as u16)
+ .sum();
+
+ if offset > 0 {
+ offset += 1
+ }
+
+ offset
+ }
+
//
pub fn offset_coords_to_in_view(
&self,
@@ -183,7 +170,7 @@ impl View {
let Position { col, row: line } =
visual_coords_at_pos(doc.text().slice(..), cursor, doc.tab_width());
- let inner_area = self.inner_area();
+ let inner_area = self.inner_area(doc);
let last_line = (self.offset.row + inner_area.height as usize).saturating_sub(1);
// - 1 so we have at least one gap in the middle.
@@ -233,10 +220,9 @@ impl View {
/// Calculates the last visible line on screen
#[inline]
pub fn last_line(&self, doc: &Document) -> usize {
- let height = self.inner_area().height;
std::cmp::min(
// Saturating subs to make it inclusive zero indexing.
- (self.offset.row + height as usize).saturating_sub(1),
+ (self.offset.row + self.inner_height()).saturating_sub(1),
doc.text().len_lines().saturating_sub(1),
)
}
@@ -270,12 +256,13 @@ impl View {
pub fn text_pos_at_screen_coords(
&self,
- text: &RopeSlice,
+ doc: &Document,
row: u16,
column: u16,
tab_width: usize,
) -> Option<usize> {
- let inner = self.inner_area();
+ let text = doc.text().slice(..);
+ let inner = self.inner_area(doc);
// 1 for status
if row < inner.top() || row >= inner.bottom() {
return None;
@@ -293,7 +280,7 @@ impl View {
let text_col = (column - inner.x) as usize + self.offset.col;
Some(pos_at_visual_coords(
- *text,
+ text,
Position {
row: text_row,
col: text_col,
@@ -305,7 +292,7 @@ impl View {
/// Translates a screen position to position in the text document.
/// Returns a usize typed position in bounds of the text if found in this view, None if out of view.
pub fn pos_at_screen_coords(&self, doc: &Document, row: u16, column: u16) -> Option<usize> {
- self.text_pos_at_screen_coords(&doc.text().slice(..), row, column, doc.tab_width())
+ self.text_pos_at_screen_coords(doc, row, column, doc.tab_width())
}
/// Translates screen coordinates into coordinates on the gutter of the view.
@@ -366,9 +353,10 @@ impl View {
mod tests {
use super::*;
use helix_core::Rope;
- const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
+ const OFFSET: u16 = 4; // 1 diagnostic + 2 linenr (< 100 lines) + 1 gutter
const OFFSET_WITHOUT_LINE_NUMBERS: u16 = 2; // 1 diagnostic + 1 gutter
// const OFFSET: u16 = GUTTERS.iter().map(|(_, width)| *width as u16).sum();
+ use crate::document::Document;
use crate::editor::GutterType;
#[test]
@@ -379,45 +367,45 @@ mod tests {
);
view.area = Rect::new(40, 40, 40, 40);
let rope = Rope::from_str("abc\n\tdef");
- let text = rope.slice(..);
+ let doc = Document::from(rope, None);
- assert_eq!(view.text_pos_at_screen_coords(&text, 40, 2, 4), None);
+ assert_eq!(view.text_pos_at_screen_coords(&doc, 40, 2, 4), None);
- assert_eq!(view.text_pos_at_screen_coords(&text, 40, 41, 4), None);
+ assert_eq!(view.text_pos_at_screen_coords(&doc, 40, 41, 4), None);
- assert_eq!(view.text_pos_at_screen_coords(&text, 0, 2, 4), None);
+ assert_eq!(view.text_pos_at_screen_coords(&doc, 0, 2, 4), None);
- assert_eq!(view.text_pos_at_screen_coords(&text, 0, 49, 4), None);
+ assert_eq!(view.text_pos_at_screen_coords(&doc, 0, 49, 4), None);
- assert_eq!(view.text_pos_at_screen_coords(&text, 0, 41, 4), None);
+ assert_eq!(view.text_pos_at_screen_coords(&doc, 0, 41, 4), None);
- assert_eq!(view.text_pos_at_screen_coords(&text, 40, 81, 4), None);
+ assert_eq!(view.text_pos_at_screen_coords(&doc, 40, 81, 4), None);
- assert_eq!(view.text_pos_at_screen_coords(&text, 78, 41, 4), None);
+ assert_eq!(view.text_pos_at_screen_coords(&doc, 78, 41, 4), None);
assert_eq!(
- view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 3, 4),
+ view.text_pos_at_screen_coords(&doc, 40, 40 + OFFSET + 3, 4),
Some(3)
);
- assert_eq!(view.text_pos_at_screen_coords(&text, 40, 80, 4), Some(3));
+ assert_eq!(view.text_pos_at_screen_coords(&doc, 40, 80, 4), Some(3));
assert_eq!(
- view.text_pos_at_screen_coords(&text, 41, 40 + OFFSET + 1, 4),
+ view.text_pos_at_screen_coords(&doc, 41, 40 + OFFSET + 1, 4),
Some(4)
);
assert_eq!(
- view.text_pos_at_screen_coords(&text, 41, 40 + OFFSET + 4, 4),
+ view.text_pos_at_screen_coords(&doc, 41, 40 + OFFSET + 4, 4),
Some(5)
);
assert_eq!(
- view.text_pos_at_screen_coords(&text, 41, 40 + OFFSET + 7, 4),
+ view.text_pos_at_screen_coords(&doc, 41, 40 + OFFSET + 7, 4),
Some(8)
);
- assert_eq!(view.text_pos_at_screen_coords(&text, 41, 80, 4), Some(8));
+ assert_eq!(view.text_pos_at_screen_coords(&doc, 41, 80, 4), Some(8));
}
#[test]
@@ -425,9 +413,9 @@ mod tests {
let mut view = View::new(DocumentId::default(), vec![GutterType::Diagnostics]);
view.area = Rect::new(40, 40, 40, 40);
let rope = Rope::from_str("abc\n\tdef");
- let text = rope.slice(..);
+ let doc = Document::from(rope, None);
assert_eq!(
- view.text_pos_at_screen_coords(&text, 41, 40 + OFFSET_WITHOUT_LINE_NUMBERS + 1, 4),
+ view.text_pos_at_screen_coords(&doc, 41, 40 + OFFSET_WITHOUT_LINE_NUMBERS + 1, 4),
Some(4)
);
}
@@ -437,11 +425,8 @@ mod tests {
let mut view = View::new(DocumentId::default(), vec![]);
view.area = Rect::new(40, 40, 40, 40);
let rope = Rope::from_str("abc\n\tdef");
- let text = rope.slice(..);
- assert_eq!(
- view.text_pos_at_screen_coords(&text, 41, 40 + 1, 4),
- Some(4)
- );
+ let doc = Document::from(rope, None);
+ assert_eq!(view.text_pos_at_screen_coords(&doc, 41, 40 + 1, 4), Some(4));
}
#[test]
@@ -452,34 +437,34 @@ mod tests {
);
view.area = Rect::new(40, 40, 40, 40);
let rope = Rope::from_str("Hi! こんにちは皆さん");
- let text = rope.slice(..);
+ let doc = Document::from(rope, None);
assert_eq!(
- view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET, 4),
+ view.text_pos_at_screen_coords(&doc, 40, 40 + OFFSET, 4),
Some(0)
);
assert_eq!(
- view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 4, 4),
+ view.text_pos_at_screen_coords(&doc, 40, 40 + OFFSET + 4, 4),
Some(4)
);
assert_eq!(
- view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 5, 4),
+ view.text_pos_at_screen_coords(&doc, 40, 40 + OFFSET + 5, 4),
Some(4)
);
assert_eq!(
- view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 6, 4),
+ view.text_pos_at_screen_coords(&doc, 40, 40 + OFFSET + 6, 4),
Some(5)
);
assert_eq!(
- view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 7, 4),
+ view.text_pos_at_screen_coords(&doc, 40, 40 + OFFSET + 7, 4),
Some(5)
);
assert_eq!(
- view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 8, 4),
+ view.text_pos_at_screen_coords(&doc, 40, 40 + OFFSET + 8, 4),
Some(6)
);
}
@@ -492,30 +477,30 @@ mod tests {
);
view.area = Rect::new(40, 40, 40, 40);
let rope = Rope::from_str("Hèl̀l̀ò world!");
- let text = rope.slice(..);
+ let doc = Document::from(rope, None);
assert_eq!(
- view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET, 4),
+ view.text_pos_at_screen_coords(&doc, 40, 40 + OFFSET, 4),
Some(0)
);
assert_eq!(
- view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 1, 4),
+ view.text_pos_at_screen_coords(&doc, 40, 40 + OFFSET + 1, 4),
Some(1)
);
assert_eq!(
- view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 2, 4),
+ view.text_pos_at_screen_coords(&doc, 40, 40 + OFFSET + 2, 4),
Some(3)
);
assert_eq!(
- view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 3, 4),
+ view.text_pos_at_screen_coords(&doc, 40, 40 + OFFSET + 3, 4),
Some(5)
);
assert_eq!(
- view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 4, 4),
+ view.text_pos_at_screen_coords(&doc, 40, 40 + OFFSET + 4, 4),
Some(7)
);
}