diff options
author | Doug Kelkhoff | 2022-11-08 12:19:59 +0000 |
---|---|---|
committer | GitHub | 2022-11-08 12:19:59 +0000 |
commit | 7ed9e9cf2567ee5e23cd8694ffccb4b38602c02a (patch) | |
tree | d815469c54dc42dc66e57fa85e389325e8b7a3a6 /helix-view | |
parent | c94feed83d746e71fb030639d740af85162b0763 (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.rs | 9 | ||||
-rw-r--r-- | helix-view/src/gutter.rs | 57 | ||||
-rw-r--r-- | helix-view/src/lib.rs | 2 | ||||
-rw-r--r-- | helix-view/src/tree.rs | 4 | ||||
-rw-r--r-- | helix-view/src/view.rs | 145 |
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) ); } |