on master help sign in
Dynamically resize line number gutter width (#3469)
7ed9e9cDoug Kelkhoff
book/src/configuration.md+1−1
@@ -46,7 +46,7 @@ on unix operating systems.
46| `line-number` | Line number display: `absolute` simply shows each line's number, while `relative` shows the distance from the current line. When unfocused or in insert mode, `relative` will still show absolute line numbers. | `absolute` |
46| `line-number` | Line number display: `absolute` simply shows each line's number, while `relative` shows the distance from the current line. When unfocused or in insert mode, `relative` will still show absolute line numbers. | `absolute` |
47| `cursorline` | Highlight all lines with a cursor. | `false` |
47| `cursorline` | Highlight all lines with a cursor. | `false` |
48| `cursorcolumn` | Highlight all columns with a cursor. | `false` |
48| `cursorcolumn` | Highlight all columns with a cursor. | `false` |
49−| `gutters` | Gutters to display: Available are `diagnostics` and `line-numbers` and `spacer`, note that `diagnostics` also includes other features like breakpoints, 1-width padding will be inserted if gutters is non-empty | `["diagnostics", "line-numbers"]` |
49+| `gutters` | Gutters to display: Available are `diagnostics` and `line-numbers` and `spacer`, note that `diagnostics` also includes other features like breakpoints, 1-width padding will be inserted if gutters is non-empty | `["diagnostics", "spacer", "line-numbers"]` |
50| `auto-completion` | Enable automatic pop up of auto-completion. | `true` |
50| `auto-completion` | Enable automatic pop up of auto-completion. | `true` |
51| `auto-format` | Enable automatic formatting on save. | `true` |
51| `auto-format` | Enable automatic formatting on save. | `true` |
52| `auto-save` | Enable automatic saving on focus moving away from Helix. Requires [focus event support](https://github.com/helix-editor/helix/wiki/Terminal-Support) from your terminal. | `false` |
52| `auto-save` | Enable automatic saving on focus moving away from Helix. Requires [focus event support](https://github.com/helix-editor/helix/wiki/Terminal-Support) from your terminal. | `false` |
helix-term/src/commands.rs+8−8
@@ -870,7 +870,7 @@ fn goto_window(cx: &mut Context, align: Align) {
870 let config = cx.editor.config();
870 let config = cx.editor.config();
871 let (view, doc) = current!(cx.editor);
871 let (view, doc) = current!(cx.editor);
872
872
873− let height = view.inner_area().height as usize;
873+ let height = view.inner_height();
874
874
875 // respect user given count if any
875 // respect user given count if any
876 // - 1 so we have at least one gap in the middle.
876 // - 1 so we have at least one gap in the middle.
@@ -1372,9 +1372,9 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
1372 return;
1372 return;
1373 }
1373 }
1374
1374
1375− let height = view.inner_area().height;
1375+ let height = view.inner_height();
1376
1376
1377− let scrolloff = config.scrolloff.min(height as usize / 2);
1377+ let scrolloff = config.scrolloff.min(height / 2);
1378
1378
1379 view.offset.row = match direction {
1379 view.offset.row = match direction {
1380 Forward => view.offset.row + offset,
1380 Forward => view.offset.row + offset,
@@ -1412,25 +1412,25 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
1412
1412
1413fn page_up(cx: &mut Context) {
1413fn page_up(cx: &mut Context) {
1414 let view = view!(cx.editor);
1414 let view = view!(cx.editor);
1415− let offset = view.inner_area().height as usize;
1415+ let offset = view.inner_height();
1416 scroll(cx, offset, Direction::Backward);
1416 scroll(cx, offset, Direction::Backward);
1417}
1417}
1418
1418
1419fn page_down(cx: &mut Context) {
1419fn page_down(cx: &mut Context) {
1420 let view = view!(cx.editor);
1420 let view = view!(cx.editor);
1421− let offset = view.inner_area().height as usize;
1421+ let offset = view.inner_height();
1422 scroll(cx, offset, Direction::Forward);
1422 scroll(cx, offset, Direction::Forward);
1423}
1423}
1424
1424
1425fn half_page_up(cx: &mut Context) {
1425fn half_page_up(cx: &mut Context) {
1426 let view = view!(cx.editor);
1426 let view = view!(cx.editor);
1427− let offset = view.inner_area().height as usize / 2;
1427+ let offset = view.inner_height() / 2;
1428 scroll(cx, offset, Direction::Backward);
1428 scroll(cx, offset, Direction::Backward);
1429}
1429}
1430
1430
1431fn half_page_down(cx: &mut Context) {
1431fn half_page_down(cx: &mut Context) {
1432 let view = view!(cx.editor);
1432 let view = view!(cx.editor);
1433− let offset = view.inner_area().height as usize / 2;
1433+ let offset = view.inner_height() / 2;
1434 scroll(cx, offset, Direction::Forward);
1434 scroll(cx, offset, Direction::Forward);
1435}
1435}
1436
1436
@@ -4342,7 +4342,7 @@ fn align_view_middle(cx: &mut Context) {
4342
4342
4343 view.offset.col = pos
4343 view.offset.col = pos
4344 .col
4344 .col
4345− .saturating_sub((view.inner_area().width as usize) / 2);
4345+ .saturating_sub((view.inner_area(doc).width as usize) / 2);
4346}
4346}
4347
4347
4348fn scroll_up(cx: &mut Context) {
4348fn scroll_up(cx: &mut Context) {
helix-term/src/ui/editor.rs+9−8
@@ -79,7 +79,7 @@ impl EditorView {
79 surface: &mut Surface,
79 surface: &mut Surface,
80 is_focused: bool,
80 is_focused: bool,
81 ) {
81 ) {
82− let inner = view.inner_area();
82+ let inner = view.inner_area(doc);
83 let area = view.area;
83 let area = view.area;
84 let theme = &editor.theme;
84 let theme = &editor.theme;
85
85
@@ -736,9 +736,10 @@ impl EditorView {
736 // avoid lots of small allocations by reusing a text buffer for each line
736 // avoid lots of small allocations by reusing a text buffer for each line
737 let mut text = String::with_capacity(8);
737 let mut text = String::with_capacity(8);
738
738
739− for (constructor, width) in view.gutters() {
739+ for gutter_type in view.gutters() {
740− let gutter = constructor(editor, doc, view, theme, is_focused, *width);
740+ let gutter = gutter_type.style(editor, doc, view, theme, is_focused);
741+ let width = gutter_type.width(view, doc);
741− text.reserve(*width); // ensure there's enough space for the gutter
742+ text.reserve(width); // ensure there's enough space for the gutter
742 for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
743 for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
743 let selected = cursors.contains(&line);
744 let selected = cursors.contains(&line);
744 let x = viewport.x + offset;
745 let x = viewport.x + offset;
@@ -751,13 +752,13 @@ impl EditorView {
751 };
752 };
752
753
753 if let Some(style) = gutter(line, selected, &mut text) {
754 if let Some(style) = gutter(line, selected, &mut text) {
754− surface.set_stringn(x, y, &text, *width, gutter_style.patch(style));
755+ surface.set_stringn(x, y, &text, width, gutter_style.patch(style));
755 } else {
756 } else {
756 surface.set_style(
757 surface.set_style(
757 Rect {
758 Rect {
758 x,
759 x,
759 y,
760 y,
760− width: *width as u16,
761+ width: width as u16,
761 height: 1,
762 height: 1,
762 },
763 },
763 gutter_style,
764 gutter_style,
@@ -766,7 +767,7 @@ impl EditorView {
766 text.clear();
767 text.clear();
767 }
768 }
768
769
769− offset += *width as u16;
770+ offset += width as u16;
770 }
771 }
771 }
772 }
772
773
@@ -882,7 +883,7 @@ impl EditorView {
882 .or_else(|| theme.try_get_exact("ui.cursorcolumn"))
883 .or_else(|| theme.try_get_exact("ui.cursorcolumn"))
883 .unwrap_or_else(|| theme.get("ui.cursorline.secondary"));
884 .unwrap_or_else(|| theme.get("ui.cursorline.secondary"));
884
885
885− let inner_area = view.inner_area();
886+ let inner_area = view.inner_area(doc);
886 let offset = view.offset.col;
887 let offset = view.offset.col;
887
888
888 let selection = doc.selection(view.id);
889 let selection = doc.selection(view.id);
helix-view/src/editor.rs+7−2
@@ -456,6 +456,7 @@ impl std::str::FromStr for GutterType {
456 fn from_str(s: &str) -> Result<Self, Self::Err> {
456 fn from_str(s: &str) -> Result<Self, Self::Err> {
457 match s.to_lowercase().as_str() {
457 match s.to_lowercase().as_str() {
458 "diagnostics" => Ok(Self::Diagnostics),
458 "diagnostics" => Ok(Self::Diagnostics),
459+ "spacer" => Ok(Self::Spacer),
459 "line-numbers" => Ok(Self::LineNumbers),
460 "line-numbers" => Ok(Self::LineNumbers),
460 _ => anyhow::bail!("Gutter type can only be `diagnostics` or `line-numbers`."),
461 _ => anyhow::bail!("Gutter type can only be `diagnostics` or `line-numbers`."),
461 }
462 }
@@ -589,7 +590,11 @@ impl Default for Config {
589 line_number: LineNumber::Absolute,
590 line_number: LineNumber::Absolute,
590 cursorline: false,
591 cursorline: false,
591 cursorcolumn: false,
592 cursorcolumn: false,
593+ gutters: vec![
592− gutters: vec![GutterType::Diagnostics, GutterType::LineNumbers],
594+ GutterType::Diagnostics,
595+ GutterType::Spacer,
596+ GutterType::LineNumbers,
597+ ],
593 middle_click_paste: true,
598 middle_click_paste: true,
594 auto_pairs: AutoPairConfig::default(),
599 auto_pairs: AutoPairConfig::default(),
595 auto_completion: true,
600 auto_completion: true,
@@ -1308,7 +1313,7 @@ impl Editor {
1308 .primary()
1313 .primary()
1309 .cursor(doc.text().slice(..));
1314 .cursor(doc.text().slice(..));
1310 if let Some(mut pos) = view.screen_coords_at_pos(doc, doc.text().slice(..), cursor) {
1315 if let Some(mut pos) = view.screen_coords_at_pos(doc, doc.text().slice(..), cursor) {
1311− let inner = view.inner_area();
1316+ let inner = view.inner_area(doc);
1312 pos.col += inner.x as usize;
1317 pos.col += inner.x as usize;
1313 pos.row += inner.y as usize;
1318 pos.row += inner.y as usize;
1314 let cursorkind = config.cursor_shape.from_mode(self.mode);
1319 let cursorkind = config.cursor_shape.from_mode(self.mode);
helix-view/src/gutter.rs+50−7
@@ -1,21 +1,54 @@
1use std::fmt::Write;
1use std::fmt::Write;
2
2
3use crate::{
3use crate::{
4+ editor::GutterType,
4 graphics::{Color, Style, UnderlineStyle},
5 graphics::{Color, Style, UnderlineStyle},
5 Document, Editor, Theme, View,
6 Document, Editor, Theme, View,
6};
7};
7
8
9+fn count_digits(n: usize) -> usize {
10+ // NOTE: if int_log gets standardized in stdlib, can use checked_log10
11+ // (https://github.com/rust-lang/rust/issues/70887#issue)
12+ std::iter::successors(Some(n), |&n| (n >= 10).then(|| n / 10)).count()
13+}
14+
8pub type GutterFn<'doc> = Box<dyn Fn(usize, bool, &mut String) -> Option<Style> + 'doc>;
15pub type GutterFn<'doc> = Box<dyn Fn(usize, bool, &mut String) -> Option<Style> + 'doc>;
9pub type Gutter =
16pub type Gutter =
10 for<'doc> fn(&'doc Editor, &'doc Document, &View, &Theme, bool, usize) -> GutterFn<'doc>;
17 for<'doc> fn(&'doc Editor, &'doc Document, &View, &Theme, bool, usize) -> GutterFn<'doc>;
11
18
19+impl GutterType {
20+ pub fn style<'doc>(
21+ self,
22+ editor: &'doc Editor,
23+ doc: &'doc Document,
24+ view: &View,
25+ theme: &Theme,
26+ is_focused: bool,
27+ ) -> GutterFn<'doc> {
28+ match self {
29+ GutterType::Diagnostics => {
30+ diagnostics_or_breakpoints(editor, doc, view, theme, is_focused)
31+ }
32+ GutterType::LineNumbers => line_numbers(editor, doc, view, theme, is_focused),
33+ GutterType::Spacer => padding(editor, doc, view, theme, is_focused),
34+ }
35+ }
36+
37+ pub fn width(self, _view: &View, doc: &Document) -> usize {
38+ match self {
39+ GutterType::Diagnostics => 1,
40+ GutterType::LineNumbers => line_numbers_width(_view, doc),
41+ GutterType::Spacer => 1,
42+ }
43+ }
44+}
45+
12pub fn diagnostic<'doc>(
46pub fn diagnostic<'doc>(
13 _editor: &'doc Editor,
47 _editor: &'doc Editor,
14 doc: &'doc Document,
48 doc: &'doc Document,
15 _view: &View,
49 _view: &View,
16 theme: &Theme,
50 theme: &Theme,
17 _is_focused: bool,
51 _is_focused: bool,
18− _width: usize,
19) -> GutterFn<'doc> {
52) -> GutterFn<'doc> {
20 let warning = theme.get("warning");
53 let warning = theme.get("warning");
21 let error = theme.get("error");
54 let error = theme.get("error");
@@ -56,10 +89,11 @@ pub fn line_numbers<'doc>(
56 view: &View,
89 view: &View,
57 theme: &Theme,
90 theme: &Theme,
58 is_focused: bool,
91 is_focused: bool,
59− width: usize,
60) -> GutterFn<'doc> {
92) -> GutterFn<'doc> {
61 let text = doc.text().slice(..);
93 let text = doc.text().slice(..);
62 let last_line = view.last_line(doc);
94 let last_line = view.last_line(doc);
95+ let width = GutterType::LineNumbers.width(view, doc);
96+
63 // Whether to draw the line number for the last line of the
97 // Whether to draw the line number for the last line of the
64 // document or not. We only draw it if it's not an empty line.
98 // document or not. We only draw it if it's not an empty line.
65 let draw_last = text.line_to_byte(last_line) < text.len_bytes();
99 let draw_last = text.line_to_byte(last_line) < text.len_bytes();
@@ -91,24 +125,35 @@ pub fn line_numbers<'doc>(
91 } else {
125 } else {
92 line + 1
126 line + 1
93 };
127 };
128+
94 let style = if selected && is_focused {
129 let style = if selected && is_focused {
95 linenr_select
130 linenr_select
96 } else {
131 } else {
97 linenr
132 linenr
98 };
133 };
134+
99 write!(out, "{:>1$}", display_num, width).unwrap();
135 write!(out, "{:>1$}", display_num, width).unwrap();
100 Some(style)
136 Some(style)
101 }
137 }
102 })
138 })
103}
139}
104
140
141+pub fn line_numbers_width(_view: &View, doc: &Document) -> usize {
142+ let text = doc.text();
143+ let last_line = text.len_lines().saturating_sub(1);
144+ let draw_last = text.line_to_byte(last_line) < text.len_bytes();
145+ let last_drawn = if draw_last { last_line + 1 } else { last_line };
146+
147+ // set a lower bound to 2-chars to minimize ambiguous relative line numbers
148+ std::cmp::max(count_digits(last_drawn), 2)
149+}
150+
105pub fn padding<'doc>(
151pub fn padding<'doc>(
106 _editor: &'doc Editor,
152 _editor: &'doc Editor,
107 _doc: &'doc Document,
153 _doc: &'doc Document,
108 _view: &View,
154 _view: &View,
109 _theme: &Theme,
155 _theme: &Theme,
110 _is_focused: bool,
156 _is_focused: bool,
111− _width: usize,
112) -> GutterFn<'doc> {
157) -> GutterFn<'doc> {
113 Box::new(|_line: usize, _selected: bool, _out: &mut String| None)
158 Box::new(|_line: usize, _selected: bool, _out: &mut String| None)
114}
159}
@@ -128,7 +173,6 @@ pub fn breakpoints<'doc>(
128 _view: &View,
173 _view: &View,
129 theme: &Theme,
174 theme: &Theme,
130 _is_focused: bool,
175 _is_focused: bool,
131− _width: usize,
132) -> GutterFn<'doc> {
176) -> GutterFn<'doc> {
133 let warning = theme.get("warning");
177 let warning = theme.get("warning");
134 let error = theme.get("error");
178 let error = theme.get("error");
@@ -181,10 +225,9 @@ pub fn diagnostics_or_breakpoints<'doc>(
181 view: &View,
225 view: &View,
182 theme: &Theme,
226 theme: &Theme,
183 is_focused: bool,
227 is_focused: bool,
184− width: usize,
185) -> GutterFn<'doc> {
228) -> GutterFn<'doc> {
186− let diagnostics = diagnostic(editor, doc, view, theme, is_focused, width);
229+ let diagnostics = diagnostic(editor, doc, view, theme, is_focused);
187− let breakpoints = breakpoints(editor, doc, view, theme, is_focused, width);
230+ let breakpoints = breakpoints(editor, doc, view, theme, is_focused);
188
231
189 Box::new(move |line, selected, out| {
232 Box::new(move |line, selected, out| {
190 breakpoints(line, selected, out).or_else(|| diagnostics(line, selected, out))
233 breakpoints(line, selected, out).or_else(|| diagnostics(line, selected, out))
helix-view/src/lib.rs+1−1
@@ -55,7 +55,7 @@ pub fn align_view(doc: &Document, view: &mut View, align: Align) {
55 .cursor(doc.text().slice(..));
55 .cursor(doc.text().slice(..));
56 let line = doc.text().char_to_line(pos);
56 let line = doc.text().char_to_line(pos);
57
57
58− let last_line_height = view.inner_area().height.saturating_sub(1) as usize;
58+ let last_line_height = view.inner_height().saturating_sub(1);
59
59
60 let relative = match align {
60 let relative = match align {
61 Align::Center => last_line_height / 2,
61 Align::Center => last_line_height / 2,
helix-view/src/tree.rs+2−2
@@ -499,7 +499,7 @@ impl Tree {
499 // in a vertical container (and already correct based on previous search)
499 // in a vertical container (and already correct based on previous search)
500 child_id = *container.children.iter().min_by_key(|id| {
500 child_id = *container.children.iter().min_by_key(|id| {
501 let x = match &self.nodes[**id].content {
501 let x = match &self.nodes[**id].content {
502− Content::View(view) => view.inner_area().left(),
502+ Content::View(view) => view.area.left(),
503 Content::Container(container) => container.area.left(),
503 Content::Container(container) => container.area.left(),
504 };
504 };
505 (current_x as i16 - x as i16).abs()
505 (current_x as i16 - x as i16).abs()
@@ -510,7 +510,7 @@ impl Tree {
510 // in a horizontal container (and already correct based on previous search)
510 // in a horizontal container (and already correct based on previous search)
511 child_id = *container.children.iter().min_by_key(|id| {
511 child_id = *container.children.iter().min_by_key(|id| {
512 let y = match &self.nodes[**id].content {
512 let y = match &self.nodes[**id].content {
513− Content::View(view) => view.inner_area().top(),
513+ Content::View(view) => view.area.top(),
514 Content::Container(container) => container.area.top(),
514 Content::Container(container) => container.area.top(),
515 };
515 };
516 (current_y as i16 - y as i16).abs()
516 (current_y as i16 - y as i16).abs()
helix-view/src/view.rs+65−80
@@ -1,8 +1,4 @@
1−use crate::{
2− graphics::Rect,
3− gutter::{self, Gutter},
4− Document, DocumentId, ViewId,
1+use crate::{editor::GutterType, graphics::Rect, Document, DocumentId, ViewId};
5−};
6use helix_core::{
2use helix_core::{
7 pos_at_visual_coords, visual_coords_at_pos, Position, RopeSlice, Selection, Transaction,
3 pos_at_visual_coords, visual_coords_at_pos, Position, RopeSlice, Selection, Transaction,
8};
4};
@@ -98,11 +94,8 @@ pub struct View {
98 pub last_modified_docs: [Option<DocumentId>; 2],
94 pub last_modified_docs: [Option<DocumentId>; 2],
99 /// used to store previous selections of tree-sitter objects
95 /// used to store previous selections of tree-sitter objects
100 pub object_selections: Vec<Selection>,
96 pub object_selections: Vec<Selection>,
101− /// Gutter (constructor) and width of gutter, used to calculate
97+ /// GutterTypes used to fetch Gutter (constructor) and width for rendering
102− /// `gutter_offset`
103− gutters: Vec<(Gutter, usize)>,
98+ gutters: Vec<GutterType>,
104− /// cached total width of gutter
105− gutter_offset: u16,
106}
99}
107
100
108impl fmt::Debug for View {
101impl fmt::Debug for View {
@@ -117,28 +110,6 @@ impl fmt::Debug for View {
117
110
118impl View {
111impl View {
119 pub fn new(doc: DocumentId, gutter_types: Vec<crate::editor::GutterType>) -> Self {
112 pub fn new(doc: DocumentId, gutter_types: Vec<crate::editor::GutterType>) -> Self {
120− let mut gutters: Vec<(Gutter, usize)> = vec![];
121− let mut gutter_offset = 0;
122− use crate::editor::GutterType;
123− for gutter_type in &gutter_types {
124− let width = match gutter_type {
125− GutterType::Diagnostics => 1,
126− GutterType::LineNumbers => 5,
127− GutterType::Spacer => 1,
128− };
129− gutter_offset += width;
130− gutters.push((
131− match gutter_type {
132− GutterType::Diagnostics => gutter::diagnostics_or_breakpoints,
133− GutterType::LineNumbers => gutter::line_numbers,
134− GutterType::Spacer => gutter::padding,
135− },
136− width as usize,
137− ));
138− }
139− if !gutter_types.is_empty() {
140− gutter_offset += 1;
141− }
142 Self {
113 Self {
143 id: ViewId::default(),
114 id: ViewId::default(),
144 doc,
115 doc,
@@ -148,8 +119,7 @@ impl View {
148 docs_access_history: Vec::new(),
119 docs_access_history: Vec::new(),
149 last_modified_docs: [None, None],
120 last_modified_docs: [None, None],
150 object_selections: Vec::new(),
121 object_selections: Vec::new(),
151− gutters,
122+ gutters: gutter_types,
152− gutter_offset,
153 }
123 }
154 }
124 }
155
125
@@ -160,15 +130,32 @@ impl View {
160 self.docs_access_history.push(id);
130 self.docs_access_history.push(id);
161 }
131 }
162
132
163− pub fn inner_area(&self) -> Rect {
133+ pub fn inner_area(&self, doc: &Document) -> Rect {
164− // TODO add abilty to not use cached offset for runtime configurable gutter
165− self.area.clip_left(self.gutter_offset).clip_bottom(1) // -1 for statusline
134+ self.area.clip_left(self.gutter_offset(doc)).clip_bottom(1) // -1 for statusline
135+ }
136+
137+ pub fn inner_height(&self) -> usize {
138+ self.area.clip_bottom(1).height.into() // -1 for statusline
166 }
139 }
167
140
168− pub fn gutters(&self) -> &[(Gutter, usize)] {
141+ pub fn gutters(&self) -> &[GutterType] {
169 &self.gutters
142 &self.gutters
170 }
143 }
171
144
145+ pub fn gutter_offset(&self, doc: &Document) -> u16 {
146+ let mut offset = self
147+ .gutters
148+ .iter()
149+ .map(|gutter| gutter.width(self, doc) as u16)
150+ .sum();
151+
152+ if offset > 0 {
153+ offset += 1
154+ }
155+
156+ offset
157+ }
158+
172 //
159 //
173 pub fn offset_coords_to_in_view(
160 pub fn offset_coords_to_in_view(
174 &self,
161 &self,
@@ -183,7 +170,7 @@ impl View {
183 let Position { col, row: line } =
170 let Position { col, row: line } =
184 visual_coords_at_pos(doc.text().slice(..), cursor, doc.tab_width());
171 visual_coords_at_pos(doc.text().slice(..), cursor, doc.tab_width());
185
172
186− let inner_area = self.inner_area();
173+ let inner_area = self.inner_area(doc);
187 let last_line = (self.offset.row + inner_area.height as usize).saturating_sub(1);
174 let last_line = (self.offset.row + inner_area.height as usize).saturating_sub(1);
188
175
189 // - 1 so we have at least one gap in the middle.
176 // - 1 so we have at least one gap in the middle.
@@ -233,10 +220,9 @@ impl View {
233 /// Calculates the last visible line on screen
220 /// Calculates the last visible line on screen
234 #[inline]
221 #[inline]
235 pub fn last_line(&self, doc: &Document) -> usize {
222 pub fn last_line(&self, doc: &Document) -> usize {
236− let height = self.inner_area().height;
237 std::cmp::min(
223 std::cmp::min(
238 // Saturating subs to make it inclusive zero indexing.
224 // Saturating subs to make it inclusive zero indexing.
239− (self.offset.row + height as usize).saturating_sub(1),
225+ (self.offset.row + self.inner_height()).saturating_sub(1),
240 doc.text().len_lines().saturating_sub(1),
226 doc.text().len_lines().saturating_sub(1),
241 )
227 )
242 }
228 }
@@ -270,12 +256,13 @@ impl View {
270
256
271 pub fn text_pos_at_screen_coords(
257 pub fn text_pos_at_screen_coords(
272 &self,
258 &self,
273− text: &RopeSlice,
259+ doc: &Document,
274 row: u16,
260 row: u16,
275 column: u16,
261 column: u16,
276 tab_width: usize,
262 tab_width: usize,
277 ) -> Option<usize> {
263 ) -> Option<usize> {
264+ let text = doc.text().slice(..);
278− let inner = self.inner_area();
265+ let inner = self.inner_area(doc);
279 // 1 for status
266 // 1 for status
280 if row < inner.top() || row >= inner.bottom() {
267 if row < inner.top() || row >= inner.bottom() {
281 return None;
268 return None;
@@ -293,7 +280,7 @@ impl View {
293 let text_col = (column - inner.x) as usize + self.offset.col;
280 let text_col = (column - inner.x) as usize + self.offset.col;
294
281
295 Some(pos_at_visual_coords(
282 Some(pos_at_visual_coords(
296− *text,
283+ text,
297 Position {
284 Position {
298 row: text_row,
285 row: text_row,
299 col: text_col,
286 col: text_col,
@@ -305,7 +292,7 @@ impl View {
305 /// Translates a screen position to position in the text document.
292 /// Translates a screen position to position in the text document.
306 /// Returns a usize typed position in bounds of the text if found in this view, None if out of view.
293 /// Returns a usize typed position in bounds of the text if found in this view, None if out of view.
307 pub fn pos_at_screen_coords(&self, doc: &Document, row: u16, column: u16) -> Option<usize> {
294 pub fn pos_at_screen_coords(&self, doc: &Document, row: u16, column: u16) -> Option<usize> {
308− self.text_pos_at_screen_coords(&doc.text().slice(..), row, column, doc.tab_width())
295+ self.text_pos_at_screen_coords(doc, row, column, doc.tab_width())
309 }
296 }
310
297
311 /// Translates screen coordinates into coordinates on the gutter of the view.
298 /// Translates screen coordinates into coordinates on the gutter of the view.
@@ -366,9 +353,10 @@ impl View {
366mod tests {
353mod tests {
367 use super::*;
354 use super::*;
368 use helix_core::Rope;
355 use helix_core::Rope;
369− const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
356+ const OFFSET: u16 = 4; // 1 diagnostic + 2 linenr (< 100 lines) + 1 gutter
370 const OFFSET_WITHOUT_LINE_NUMBERS: u16 = 2; // 1 diagnostic + 1 gutter
357 const OFFSET_WITHOUT_LINE_NUMBERS: u16 = 2; // 1 diagnostic + 1 gutter
371 // const OFFSET: u16 = GUTTERS.iter().map(|(_, width)| *width as u16).sum();
358 // const OFFSET: u16 = GUTTERS.iter().map(|(_, width)| *width as u16).sum();
359+ use crate::document::Document;
372 use crate::editor::GutterType;
360 use crate::editor::GutterType;
373
361
374 #[test]
362 #[test]
@@ -379,45 +367,45 @@ mod tests {
379 );
367 );
380 view.area = Rect::new(40, 40, 40, 40);
368 view.area = Rect::new(40, 40, 40, 40);
381 let rope = Rope::from_str("abc\n\tdef");
369 let rope = Rope::from_str("abc\n\tdef");
382− let text = rope.slice(..);
370+ let doc = Document::from(rope, None);
383
371
384− assert_eq!(view.text_pos_at_screen_coords(&text, 40, 2, 4), None);
372+ assert_eq!(view.text_pos_at_screen_coords(&doc, 40, 2, 4), None);
385
373
386− assert_eq!(view.text_pos_at_screen_coords(&text, 40, 41, 4), None);
374+ assert_eq!(view.text_pos_at_screen_coords(&doc, 40, 41, 4), None);
387
375
388− assert_eq!(view.text_pos_at_screen_coords(&text, 0, 2, 4), None);
376+ assert_eq!(view.text_pos_at_screen_coords(&doc, 0, 2, 4), None);
389
377
390− assert_eq!(view.text_pos_at_screen_coords(&text, 0, 49, 4), None);
378+ assert_eq!(view.text_pos_at_screen_coords(&doc, 0, 49, 4), None);
391
379
392− assert_eq!(view.text_pos_at_screen_coords(&text, 0, 41, 4), None);
380+ assert_eq!(view.text_pos_at_screen_coords(&doc, 0, 41, 4), None);
393
381
394− assert_eq!(view.text_pos_at_screen_coords(&text, 40, 81, 4), None);
382+ assert_eq!(view.text_pos_at_screen_coords(&doc, 40, 81, 4), None);
395
383
396− assert_eq!(view.text_pos_at_screen_coords(&text, 78, 41, 4), None);
384+ assert_eq!(view.text_pos_at_screen_coords(&doc, 78, 41, 4), None);
397
385
398 assert_eq!(
386 assert_eq!(
399− view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 3, 4),
387+ view.text_pos_at_screen_coords(&doc, 40, 40 + OFFSET + 3, 4),
400 Some(3)
388 Some(3)
401 );
389 );
402
390
403− assert_eq!(view.text_pos_at_screen_coords(&text, 40, 80, 4), Some(3));
391+ assert_eq!(view.text_pos_at_screen_coords(&doc, 40, 80, 4), Some(3));
404
392
405 assert_eq!(
393 assert_eq!(
406− view.text_pos_at_screen_coords(&text, 41, 40 + OFFSET + 1, 4),
394+ view.text_pos_at_screen_coords(&doc, 41, 40 + OFFSET + 1, 4),
407 Some(4)
395 Some(4)
408 );
396 );
409
397
410 assert_eq!(
398 assert_eq!(
411− view.text_pos_at_screen_coords(&text, 41, 40 + OFFSET + 4, 4),
399+ view.text_pos_at_screen_coords(&doc, 41, 40 + OFFSET + 4, 4),
412 Some(5)
400 Some(5)
413 );
401 );
414
402
415 assert_eq!(
403 assert_eq!(
416− view.text_pos_at_screen_coords(&text, 41, 40 + OFFSET + 7, 4),
404+ view.text_pos_at_screen_coords(&doc, 41, 40 + OFFSET + 7, 4),
417 Some(8)
405 Some(8)
418 );
406 );
419
407
420− assert_eq!(view.text_pos_at_screen_coords(&text, 41, 80, 4), Some(8));
408+ assert_eq!(view.text_pos_at_screen_coords(&doc, 41, 80, 4), Some(8));
421 }
409 }
422
410
423 #[test]
411 #[test]
@@ -425,9 +413,9 @@ mod tests {
425 let mut view = View::new(DocumentId::default(), vec![GutterType::Diagnostics]);
413 let mut view = View::new(DocumentId::default(), vec![GutterType::Diagnostics]);
426 view.area = Rect::new(40, 40, 40, 40);
414 view.area = Rect::new(40, 40, 40, 40);
427 let rope = Rope::from_str("abc\n\tdef");
415 let rope = Rope::from_str("abc\n\tdef");
428− let text = rope.slice(..);
416+ let doc = Document::from(rope, None);
429 assert_eq!(
417 assert_eq!(
430− view.text_pos_at_screen_coords(&text, 41, 40 + OFFSET_WITHOUT_LINE_NUMBERS + 1, 4),
418+ view.text_pos_at_screen_coords(&doc, 41, 40 + OFFSET_WITHOUT_LINE_NUMBERS + 1, 4),
431 Some(4)
419 Some(4)
432 );
420 );
433 }
421 }
@@ -437,11 +425,8 @@ mod tests {
437 let mut view = View::new(DocumentId::default(), vec![]);
425 let mut view = View::new(DocumentId::default(), vec![]);
438 view.area = Rect::new(40, 40, 40, 40);
426 view.area = Rect::new(40, 40, 40, 40);
439 let rope = Rope::from_str("abc\n\tdef");
427 let rope = Rope::from_str("abc\n\tdef");
440− let text = rope.slice(..);
428+ let doc = Document::from(rope, None);
441− assert_eq!(
442− view.text_pos_at_screen_coords(&text, 41, 40 + 1, 4),
429+ assert_eq!(view.text_pos_at_screen_coords(&doc, 41, 40 + 1, 4), Some(4));
443− Some(4)
444− );
445 }
430 }
446
431
447 #[test]
432 #[test]
@@ -452,34 +437,34 @@ mod tests {
452 );
437 );
453 view.area = Rect::new(40, 40, 40, 40);
438 view.area = Rect::new(40, 40, 40, 40);
454 let rope = Rope::from_str("Hi! こんにちは皆さん");
439 let rope = Rope::from_str("Hi! こんにちは皆さん");
455− let text = rope.slice(..);
440+ let doc = Document::from(rope, None);
456
441
457 assert_eq!(
442 assert_eq!(
458− view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET, 4),
443+ view.text_pos_at_screen_coords(&doc, 40, 40 + OFFSET, 4),
459 Some(0)
444 Some(0)
460 );
445 );
461
446
462 assert_eq!(
447 assert_eq!(
463− view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 4, 4),
448+ view.text_pos_at_screen_coords(&doc, 40, 40 + OFFSET + 4, 4),
464 Some(4)
449 Some(4)
465 );
450 );
466 assert_eq!(
451 assert_eq!(
467− view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 5, 4),
452+ view.text_pos_at_screen_coords(&doc, 40, 40 + OFFSET + 5, 4),
468 Some(4)
453 Some(4)
469 );
454 );
470
455
471 assert_eq!(
456 assert_eq!(
472− view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 6, 4),
457+ view.text_pos_at_screen_coords(&doc, 40, 40 + OFFSET + 6, 4),
473 Some(5)
458 Some(5)
474 );
459 );
475
460
476 assert_eq!(
461 assert_eq!(
477− view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 7, 4),
462+ view.text_pos_at_screen_coords(&doc, 40, 40 + OFFSET + 7, 4),
478 Some(5)
463 Some(5)
479 );
464 );
480
465
481 assert_eq!(
466 assert_eq!(
482− view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 8, 4),
467+ view.text_pos_at_screen_coords(&doc, 40, 40 + OFFSET + 8, 4),
483 Some(6)
468 Some(6)
484 );
469 );
485 }
470 }
@@ -492,30 +477,30 @@ mod tests {
492 );
477 );
493 view.area = Rect::new(40, 40, 40, 40);
478 view.area = Rect::new(40, 40, 40, 40);
494 let rope = Rope::from_str("Hèl̀l̀ò world!");
479 let rope = Rope::from_str("Hèl̀l̀ò world!");
495− let text = rope.slice(..);
480+ let doc = Document::from(rope, None);
496
481
497 assert_eq!(
482 assert_eq!(
498− view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET, 4),
483+ view.text_pos_at_screen_coords(&doc, 40, 40 + OFFSET, 4),
499 Some(0)
484 Some(0)
500 );
485 );
501
486
502 assert_eq!(
487 assert_eq!(
503− view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 1, 4),
488+ view.text_pos_at_screen_coords(&doc, 40, 40 + OFFSET + 1, 4),
504 Some(1)
489 Some(1)
505 );
490 );
506
491
507 assert_eq!(
492 assert_eq!(
508− view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 2, 4),
493+ view.text_pos_at_screen_coords(&doc, 40, 40 + OFFSET + 2, 4),
509 Some(3)
494 Some(3)
510 );
495 );
511
496
512 assert_eq!(
497 assert_eq!(
513− view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 3, 4),
498+ view.text_pos_at_screen_coords(&doc, 40, 40 + OFFSET + 3, 4),
514 Some(5)
499 Some(5)
515 );
500 );
516
501
517 assert_eq!(
502 assert_eq!(
518− view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 4, 4),
503+ view.text_pos_at_screen_coords(&doc, 40, 40 + OFFSET + 4, 4),
519 Some(7)
504 Some(7)
520 );
505 );
521 }
506 }