Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamically resize line number gutter width #3469

Merged
merged 14 commits into from
Nov 8, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions helix-term/src/ui/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -638,22 +638,23 @@ 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.row_styler(editor, doc, view, theme, is_focused);
dgkf marked this conversation as resolved.
Show resolved Hide resolved
let width = gutter_type.width(view);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

width is no longer passed to gutter_type.row_styler (formerly constructor), though I could see the argument for calculating width first and providing it to the row_styler.

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;
let y = viewport.y + i as u16;

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 @@ -662,7 +663,7 @@ impl EditorView {
text.clear();
}

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

Expand Down
66 changes: 58 additions & 8 deletions helix-view/src/gutter.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,53 @@
use std::fmt::Write;
use std::iter::successors;

use crate::{
editor::GutterType,
graphics::{Color, Modifier, Style},
Document, Editor, Theme, View,
};

fn count_digits(n: usize) -> usize {
successors(Some(n), |&n| (n >= 10).then(|| n / 10)).count()
}
dgkf marked this conversation as resolved.
Show resolved Hide resolved

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 row_styler<'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) -> usize {
match self {
GutterType::Diagnostics => 1,
GutterType::LineNumbers => line_numbers_width(view),
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 +88,13 @@ pub fn line_numbers<'doc>(
view: &View,
theme: &Theme,
is_focused: bool,
width: usize,
) -> GutterFn<'doc> {
const ELLIPSIS: char = '\u{2026}';

let text = doc.text().slice(..);
let last_line = view.last_line(doc);
let width = line_numbers_width(view);
dgkf marked this conversation as resolved.
Show resolved Hide resolved

// 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 +126,41 @@ pub fn line_numbers<'doc>(
} else {
line + 1
};

let n_digits = count_digits(display_num);

let style = if selected && is_focused {
linenr_select
} else {
linenr
};
write!(out, "{:>1$}", display_num, width).unwrap();

// if line number overflows maximum alotted width, truncate start
if n_digits > width {
let display_trailing = (display_num as u32) % 10_u32.pow((width - 1) as u32);
write!(out, "{}{:0>2$}", ELLIPSIS, display_trailing, width - 1).unwrap();
} else {
write!(out, "{:>1$}", display_num, width).unwrap();
}
dgkf marked this conversation as resolved.
Show resolved Hide resolved

Some(style)
}
})
}

pub fn line_numbers_width(view: &View) -> usize {
// TODO: allow gutter widths to be dependent on Document. Currently the
// width is based on full View height, not visible line numbers.
let last_view_line = view.offset.row + view.area.bottom() as usize;
std::cmp::max(std::cmp::min(count_digits(last_view_line), 5), 3)
}
dgkf marked this conversation as resolved.
Show resolved Hide resolved

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 +180,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 +232,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
50 changes: 16 additions & 34 deletions helix-view/src/view.rs
Original file line number Diff line number Diff line change
@@ -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};

use std::fmt;
Expand Down Expand Up @@ -82,9 +78,7 @@ pub struct View {
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,
gutters: Vec<GutterType>,
}

impl fmt::Debug for View {
Expand All @@ -99,28 +93,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,
Expand All @@ -130,8 +102,7 @@ impl View {
docs_access_history: Vec::new(),
last_modified_docs: [None, None],
object_selections: Vec::new(),
gutters,
gutter_offset,
gutters: gutter_types,
}
}

Expand All @@ -144,13 +115,24 @@ impl View {

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
self.area.clip_left(self.gutter_offset()).clip_bottom(1) // -1 for statusline
dgkf marked this conversation as resolved.
Show resolved Hide resolved
}

pub fn gutters(&self) -> &[(Gutter, usize)] {
pub fn gutters(&self) -> &[GutterType] {
&self.gutters
}

pub fn gutter_offset(&self) -> u16 {
let mut offset = 0;
for gutter in self.gutters.iter() {
dgkf marked this conversation as resolved.
Show resolved Hide resolved
offset += gutter.width(self) as u16
}
if offset > 0 {
offset += 1
}
offset
}

//
pub fn offset_coords_to_in_view(
&self,
Expand Down