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 all commits
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
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 @@ -1360,9 +1360,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 @@ -1400,25 +1400,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 @@ -4306,7 +4306,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 @@ -1304,7 +1309,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
dgkf marked this conversation as resolved.
Show resolved Hide resolved
// (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 @@ -53,7 +53,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(),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the only place where the Document isn't easily available to calculate the inner area (now that the gutter width is based on the Document contents). A simple solution is to use the entire view area instead.

I think this would affect things like moving to a split when the screen location that you would move to would be in the gutter of a neighboring split. Previously, if you were to try to find a neighboring split at a gutter, you would get the window to the left, now you would get the window that contains the gutter.

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