Skip to content

Commit 9237547

Browse files
dgkfShekhinah Memmel
authored andcommitted
Dynamically resize line number gutter width (helix-editor#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
1 parent 4c798ed commit 9237547

File tree

8 files changed

+143
-109
lines changed

8 files changed

+143
-109
lines changed

book/src/configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ on unix operating systems.
4646
| `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` |
4747
| `cursorline` | Highlight all lines with a cursor. | `false` |
4848
| `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"]` |
5050
| `auto-completion` | Enable automatic pop up of auto-completion. | `true` |
5151
| `auto-format` | Enable automatic formatting on save. | `true` |
5252
| `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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -873,7 +873,7 @@ fn goto_window(cx: &mut Context, align: Align) {
873873
let config = cx.editor.config();
874874
let (view, doc) = current!(cx.editor);
875875

876-
let height = view.inner_area().height as usize;
876+
let height = view.inner_height();
877877

878878
// respect user given count if any
879879
// - 1 so we have at least one gap in the middle.
@@ -1375,9 +1375,9 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
13751375
return;
13761376
}
13771377

1378-
let height = view.inner_area().height;
1378+
let height = view.inner_height();
13791379

1380-
let scrolloff = config.scrolloff.min(height as usize / 2);
1380+
let scrolloff = config.scrolloff.min(height / 2);
13811381

13821382
view.offset.row = match direction {
13831383
Forward => view.offset.row + offset,
@@ -1415,25 +1415,25 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
14151415

14161416
fn page_up(cx: &mut Context) {
14171417
let view = view!(cx.editor);
1418-
let offset = view.inner_area().height as usize;
1418+
let offset = view.inner_height();
14191419
scroll(cx, offset, Direction::Backward);
14201420
}
14211421

14221422
fn page_down(cx: &mut Context) {
14231423
let view = view!(cx.editor);
1424-
let offset = view.inner_area().height as usize;
1424+
let offset = view.inner_height();
14251425
scroll(cx, offset, Direction::Forward);
14261426
}
14271427

14281428
fn half_page_up(cx: &mut Context) {
14291429
let view = view!(cx.editor);
1430-
let offset = view.inner_area().height as usize / 2;
1430+
let offset = view.inner_height() / 2;
14311431
scroll(cx, offset, Direction::Backward);
14321432
}
14331433

14341434
fn half_page_down(cx: &mut Context) {
14351435
let view = view!(cx.editor);
1436-
let offset = view.inner_area().height as usize / 2;
1436+
let offset = view.inner_height() / 2;
14371437
scroll(cx, offset, Direction::Forward);
14381438
}
14391439

@@ -4382,7 +4382,7 @@ fn align_view_middle(cx: &mut Context) {
43824382

43834383
view.offset.col = pos
43844384
.col
4385-
.saturating_sub((view.inner_area().width as usize) / 2);
4385+
.saturating_sub((view.inner_area(doc).width as usize) / 2);
43864386
}
43874387

43884388
fn scroll_up(cx: &mut Context) {

helix-term/src/ui/editor.rs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ impl EditorView {
8181
surface: &mut Surface,
8282
is_focused: bool,
8383
) {
84-
let inner = view.inner_area();
84+
let inner = view.inner_area(doc);
8585
let area = view.area;
8686
let theme = &editor.theme;
8787

@@ -738,9 +738,10 @@ impl EditorView {
738738
// avoid lots of small allocations by reusing a text buffer for each line
739739
let mut text = String::with_capacity(8);
740740

741-
for (constructor, width) in view.gutters() {
742-
let gutter = constructor(editor, doc, view, theme, is_focused, *width);
743-
text.reserve(*width); // ensure there's enough space for the gutter
741+
for gutter_type in view.gutters() {
742+
let gutter = gutter_type.style(editor, doc, view, theme, is_focused);
743+
let width = gutter_type.width(view, doc);
744+
text.reserve(width); // ensure there's enough space for the gutter
744745
for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
745746
let selected = cursors.contains(&line);
746747
let x = viewport.x + offset;
@@ -753,13 +754,13 @@ impl EditorView {
753754
};
754755

755756
if let Some(style) = gutter(line, selected, &mut text) {
756-
surface.set_stringn(x, y, &text, *width, gutter_style.patch(style));
757+
surface.set_stringn(x, y, &text, width, gutter_style.patch(style));
757758
} else {
758759
surface.set_style(
759760
Rect {
760761
x,
761762
y,
762-
width: *width as u16,
763+
width: width as u16,
763764
height: 1,
764765
},
765766
gutter_style,
@@ -768,7 +769,7 @@ impl EditorView {
768769
text.clear();
769770
}
770771

771-
offset += *width as u16;
772+
offset += width as u16;
772773
}
773774
}
774775

@@ -884,7 +885,7 @@ impl EditorView {
884885
.or_else(|| theme.try_get_exact("ui.cursorcolumn"))
885886
.unwrap_or_else(|| theme.get("ui.cursorline.secondary"));
886887

887-
let inner_area = view.inner_area();
888+
let inner_area = view.inner_area(doc);
888889
let offset = view.offset.col;
889890

890891
let selection = doc.selection(view.id);

helix-view/src/editor.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,7 @@ impl std::str::FromStr for GutterType {
521521
fn from_str(s: &str) -> Result<Self, Self::Err> {
522522
match s.to_lowercase().as_str() {
523523
"diagnostics" => Ok(Self::Diagnostics),
524+
"spacer" => Ok(Self::Spacer),
524525
"line-numbers" => Ok(Self::LineNumbers),
525526
_ => anyhow::bail!("Gutter type can only be `diagnostics` or `line-numbers`."),
526527
}
@@ -654,7 +655,11 @@ impl Default for Config {
654655
line_number: LineNumber::Absolute,
655656
cursorline: false,
656657
cursorcolumn: false,
657-
gutters: vec![GutterType::Diagnostics, GutterType::LineNumbers],
658+
gutters: vec![
659+
GutterType::Diagnostics,
660+
GutterType::Spacer,
661+
GutterType::LineNumbers,
662+
],
658663
middle_click_paste: true,
659664
auto_pairs: AutoPairConfig::default(),
660665
auto_completion: true,
@@ -1374,7 +1379,7 @@ impl Editor {
13741379
.primary()
13751380
.cursor(doc.text().slice(..));
13761381
if let Some(mut pos) = view.screen_coords_at_pos(doc, doc.text().slice(..), cursor) {
1377-
let inner = view.inner_area();
1382+
let inner = view.inner_area(doc);
13781383
pos.col += inner.x as usize;
13791384
pos.row += inner.y as usize;
13801385
let cursorkind = config.cursor_shape.from_mode(self.mode);

helix-view/src/gutter.rs

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,54 @@
11
use std::fmt::Write;
22

33
use crate::{
4+
editor::GutterType,
45
graphics::{Color, Style, UnderlineStyle},
56
Document, Editor, Theme, View,
67
};
78

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+
815
pub type GutterFn<'doc> = Box<dyn Fn(usize, bool, &mut String) -> Option<Style> + 'doc>;
916
pub type Gutter =
1017
for<'doc> fn(&'doc Editor, &'doc Document, &View, &Theme, bool, usize) -> GutterFn<'doc>;
1118

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+
1246
pub fn diagnostic<'doc>(
1347
_editor: &'doc Editor,
1448
doc: &'doc Document,
1549
_view: &View,
1650
theme: &Theme,
1751
_is_focused: bool,
18-
_width: usize,
1952
) -> GutterFn<'doc> {
2053
let warning = theme.get("warning");
2154
let error = theme.get("error");
@@ -56,10 +89,11 @@ pub fn line_numbers<'doc>(
5689
view: &View,
5790
theme: &Theme,
5891
is_focused: bool,
59-
width: usize,
6092
) -> GutterFn<'doc> {
6193
let text = doc.text().slice(..);
6294
let last_line = view.last_line(doc);
95+
let width = GutterType::LineNumbers.width(view, doc);
96+
6397
// Whether to draw the line number for the last line of the
6498
// document or not. We only draw it if it's not an empty line.
6599
let draw_last = text.line_to_byte(last_line) < text.len_bytes();
@@ -91,24 +125,35 @@ pub fn line_numbers<'doc>(
91125
} else {
92126
line + 1
93127
};
128+
94129
let style = if selected && is_focused {
95130
linenr_select
96131
} else {
97132
linenr
98133
};
134+
99135
write!(out, "{:>1$}", display_num, width).unwrap();
100136
Some(style)
101137
}
102138
})
103139
}
104140

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+
105151
pub fn padding<'doc>(
106152
_editor: &'doc Editor,
107153
_doc: &'doc Document,
108154
_view: &View,
109155
_theme: &Theme,
110156
_is_focused: bool,
111-
_width: usize,
112157
) -> GutterFn<'doc> {
113158
Box::new(|_line: usize, _selected: bool, _out: &mut String| None)
114159
}
@@ -128,7 +173,6 @@ pub fn breakpoints<'doc>(
128173
_view: &View,
129174
theme: &Theme,
130175
_is_focused: bool,
131-
_width: usize,
132176
) -> GutterFn<'doc> {
133177
let warning = theme.get("warning");
134178
let error = theme.get("error");
@@ -181,10 +225,9 @@ pub fn diagnostics_or_breakpoints<'doc>(
181225
view: &View,
182226
theme: &Theme,
183227
is_focused: bool,
184-
width: usize,
185228
) -> GutterFn<'doc> {
186-
let diagnostics = diagnostic(editor, doc, view, theme, is_focused, width);
187-
let breakpoints = breakpoints(editor, doc, view, theme, is_focused, width);
229+
let diagnostics = diagnostic(editor, doc, view, theme, is_focused);
230+
let breakpoints = breakpoints(editor, doc, view, theme, is_focused);
188231

189232
Box::new(move |line, selected, out| {
190233
breakpoints(line, selected, out).or_else(|| diagnostics(line, selected, out))

helix-view/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ pub fn align_view(doc: &Document, view: &mut View, align: Align) {
5555
.cursor(doc.text().slice(..));
5656
let line = doc.text().char_to_line(pos);
5757

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);
5959

6060
let relative = match align {
6161
Align::Center => last_line_height / 2,

helix-view/src/tree.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -499,7 +499,7 @@ impl Tree {
499499
// in a vertical container (and already correct based on previous search)
500500
child_id = *container.children.iter().min_by_key(|id| {
501501
let x = match &self.nodes[**id].content {
502-
Content::View(view) => view.inner_area().left(),
502+
Content::View(view) => view.area.left(),
503503
Content::Container(container) => container.area.left(),
504504
};
505505
(current_x as i16 - x as i16).abs()
@@ -510,7 +510,7 @@ impl Tree {
510510
// in a horizontal container (and already correct based on previous search)
511511
child_id = *container.children.iter().min_by_key(|id| {
512512
let y = match &self.nodes[**id].content {
513-
Content::View(view) => view.inner_area().top(),
513+
Content::View(view) => view.area.top(),
514514
Content::Container(container) => container.area.top(),
515515
};
516516
(current_y as i16 - y as i16).abs()

0 commit comments

Comments
 (0)