Skip to content

Commit

Permalink
Dynamically resize line number gutter width (#3469)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
dgkf authored Nov 8, 2022
1 parent c94feed commit 7ed9e9c
Show file tree
Hide file tree
Showing 8 changed files with 143 additions and 109 deletions.
2 changes: 1 addition & 1 deletion book/src/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ on unix operating systems.
| `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` |
| `cursorline` | Highlight all lines with a cursor. | `false` |
| `cursorcolumn` | Highlight all columns with a cursor. | `false` |
| `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"]` |
| `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"]` |
| `auto-completion` | Enable automatic pop up of auto-completion. | `true` |
| `auto-format` | Enable automatic formatting on save. | `true` |
| `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` |
Expand Down
16 changes: 8 additions & 8 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -870,7 +870,7 @@ fn goto_window(cx: &mut Context, align: Align) {
let config = cx.editor.config();
let (view, doc) = current!(cx.editor);

let height = view.inner_area().height as usize;
let height = view.inner_height();

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

let height = view.inner_area().height;
let height = view.inner_height();

let scrolloff = config.scrolloff.min(height as usize / 2);
let scrolloff = config.scrolloff.min(height / 2);

view.offset.row = match direction {
Forward => view.offset.row + offset,
Expand Down Expand Up @@ -1412,25 +1412,25 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) {

fn page_up(cx: &mut Context) {
let view = view!(cx.editor);
let offset = view.inner_area().height as usize;
let offset = view.inner_height();
scroll(cx, offset, Direction::Backward);
}

fn page_down(cx: &mut Context) {
let view = view!(cx.editor);
let offset = view.inner_area().height as usize;
let offset = view.inner_height();
scroll(cx, offset, Direction::Forward);
}

fn half_page_up(cx: &mut Context) {
let view = view!(cx.editor);
let offset = view.inner_area().height as usize / 2;
let offset = view.inner_height() / 2;
scroll(cx, offset, Direction::Backward);
}

fn half_page_down(cx: &mut Context) {
let view = view!(cx.editor);
let offset = view.inner_area().height as usize / 2;
let offset = view.inner_height() / 2;
scroll(cx, offset, Direction::Forward);
}

Expand Down Expand Up @@ -4342,7 +4342,7 @@ fn align_view_middle(cx: &mut Context) {

view.offset.col = pos
.col
.saturating_sub((view.inner_area().width as usize) / 2);
.saturating_sub((view.inner_area(doc).width as usize) / 2);
}

fn scroll_up(cx: &mut Context) {
Expand Down
17 changes: 9 additions & 8 deletions helix-term/src/ui/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ impl EditorView {
surface: &mut Surface,
is_focused: bool,
) {
let inner = view.inner_area();
let inner = view.inner_area(doc);
let area = view.area;
let theme = &editor.theme;

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

for (constructor, width) in view.gutters() {
let gutter = constructor(editor, doc, view, theme, is_focused, *width);
text.reserve(*width); // ensure there's enough space for the gutter
for gutter_type in view.gutters() {
let gutter = gutter_type.style(editor, doc, view, theme, is_focused);
let width = gutter_type.width(view, doc);
text.reserve(width); // ensure there's enough space for the gutter
for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
let selected = cursors.contains(&line);
let x = viewport.x + offset;
Expand All @@ -751,13 +752,13 @@ impl EditorView {
};

if let Some(style) = gutter(line, selected, &mut text) {
surface.set_stringn(x, y, &text, *width, gutter_style.patch(style));
surface.set_stringn(x, y, &text, width, gutter_style.patch(style));
} else {
surface.set_style(
Rect {
x,
y,
width: *width as u16,
width: width as u16,
height: 1,
},
gutter_style,
Expand All @@ -766,7 +767,7 @@ impl EditorView {
text.clear();
}

offset += *width as u16;
offset += width as u16;
}
}

Expand Down Expand Up @@ -882,7 +883,7 @@ impl EditorView {
.or_else(|| theme.try_get_exact("ui.cursorcolumn"))
.unwrap_or_else(|| theme.get("ui.cursorline.secondary"));

let inner_area = view.inner_area();
let inner_area = view.inner_area(doc);
let offset = view.offset.col;

let selection = doc.selection(view.id);
Expand Down
9 changes: 7 additions & 2 deletions helix-view/src/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`."),
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down
57 changes: 50 additions & 7 deletions helix-view/src/gutter.rs
Original file line number Diff line number Diff line change
@@ -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");
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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)
}
Expand All @@ -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");
Expand Down Expand Up @@ -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))
Expand Down
2 changes: 1 addition & 1 deletion helix-view/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions helix-view/src/tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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()
Expand Down
Loading

0 comments on commit 7ed9e9c

Please sign in to comment.