fix: add spacing between line numbers and repository markers

This commit is contained in:
2026-10-13 21:02:34 +02:00
parent fd59c05f80
commit e90d1c3945
4 changed files with 19 additions and 16 deletions
+2 -2
View File
@@ -84,9 +84,9 @@ Already-ready mouse reports share a redraw: at most 64 reports are applied in th
Ratatui's bounded layout cache is enabled: unchanged geometry and constraints reuse solved rectangles during scrolling; resizes and panel-layout changes use their own cache keys. Ratatui's bounded layout cache is enabled: unchanged geometry and constraints reuse solved rectangles during scrolling; resizes and panel-layout changes use their own cache keys.
The buffer viewport includes a fixed left gutter with right-aligned hybrid line numbers: the highlighted primary cursor line shows its one-based absolute number; every other line shows its distance from the primary. Secondary carets do not establish separate numbering origins. Width follows the total line count plus one separator cell, including an empty final row after a trailing newline; `git_gutter` bar/symbol modes add one marker cell between the number and separator. Number-color/none modes add no width. Horizontal scrolling affects only text; search/selection highlights and carets never paint over the gutter. Wrapped continuations and rows beyond the buffer have no number or marker. The gutter is hidden when it would leave no text column. The buffer viewport includes a fixed left gutter with right-aligned hybrid line numbers: the highlighted primary cursor line shows its one-based absolute number; every other line shows its distance from the primary. Secondary carets do not establish separate numbering origins. Width follows the total line count plus one separator cell, including an empty final row after a trailing newline; `git_gutter` bar/symbol modes add a blank cell and a marker cell between the number and separator. Number-color/none modes add no width. Horizontal scrolling affects only text; search/selection highlights and carets never paint over the gutter. Wrapped continuations and rows beyond the buffer have no number or marker. The gutter is hidden when it would leave no text column.
Repository gutter work runs off the render/editor thread. Revision/path-validated marker vectors belong to ordinary source buffers, not historical snapshots; panes and tabs borrow their own buffer's markers. The worker compares editor text with Git HEAD or Git-backed jj's single parent, independently of save baselines and index state. The frontend paints green additions, red deletion anchors, and yellow modifications as `▐`, symbols, or number colors; current-line cyan/bold overrides repository colors in number-color mode. Repository gutter work runs off the render/editor thread. Revision/path-validated marker vectors belong to ordinary source buffers, not historical snapshots; panes and tabs borrow their own buffer's markers. The worker compares editor text with Git HEAD or Git-backed jj's single parent, independently of save baselines and index state. The frontend paints green additions, red deletion anchors, and yellow modifications as `▌`, symbols, or number colors; current-line cyan/bold overrides repository colors in number-color mode.
## Syntax caches ## Syntax caches
+1 -1
View File
@@ -49,7 +49,7 @@ Plugin paths resolve relative to the config file, not the shell's working direct
`config.git_blame` defaults to `true`; set it to `false` to disable current-line inline Git blame and its background worker. Only literal Ruby booleans are accepted; other values reject the configuration. Toggle it for the running session with `:git_blame.toggle`, its palette action, or a custom binding such as `config.bind "leader b", config.core.git_blame.toggle`. There is no default shortcut. `config.git_blame` defaults to `true`; set it to `false` to disable current-line inline Git blame and its background worker. Only literal Ruby booleans are accepted; other values reject the configuration. Toggle it for the running session with `:git_blame.toggle`, its palette action, or a custom binding such as `config.bind "leader b", config.core.git_blame.toggle`. There is no default shortcut.
`config.git_gutter` defaults to `"bar"`: a fat `▐` occupying the right half of the marker cell, visually separated from the line number without widening the gutter, followed by a blank before the text. `"symbols"` uses `+` / `-` / `~` instead. Added, removed, and modified lines use green, red, and yellow respectively. `"line_numbers"` colors the existing numbers without adding a marker column; the current line retains its cyan/bold caret highlight. `"none"` disables repository indicators, not line numbers. Other modes retain diagnostic number colors independently of repository markers. Unsupported strings, empty strings, and non-string values reject the configuration. `config.git_gutter` defaults to `"bar"`: a fat `▌` with one blank column between the line number and marker and another before the text. `"symbols"` uses `+` / `-` / `~` with the same spacing. Added, removed, and modified lines use green, red, and yellow respectively. `"line_numbers"` colors the existing numbers without adding a marker column; the current line retains its cyan/bold caret highlight. `"none"` disables repository indicators, not line numbers. Other modes retain diagnostic number colors independently of repository markers. Unsupported strings, empty strings, and non-string values reject the configuration.
Indicators compare the current editor text (including unsaved edits) against Git `HEAD`, or the parent `@-` in Git-backed jj repositories, rather than against the last save or Git index. Line endings use the editor's LF normalization. They refresh after edits/saves, on focus regain, and once per second for repository changes. Historical snapshots do not receive live indicators. Eligible named files not yet on disk show nonempty unsaved text as added; tracked files missing on disk still compare their buffer text against the baseline. Non-repository, ignored, binary, symlink, and unsupported files have no indicators; neither do jj working changes with multiple parents. Both colocated and non-colocated Git-backed jj repositories are supported. Files above 16 MiB report an error and clear their indicators; request batches are limited to 64 MiB or 1024 files. Indicators compare the current editor text (including unsaved edits) against Git `HEAD`, or the parent `@-` in Git-backed jj repositories, rather than against the last save or Git index. Line endings use the editor's LF normalization. They refresh after edits/saves, on focus regain, and once per second for repository changes. Historical snapshots do not receive live indicators. Eligible named files not yet on disk show nonempty unsaved text as added; tracked files missing on disk still compare their buffer text against the baseline. Non-repository, ignored, binary, symlink, and unsupported files have no indicators; neither do jj working changes with multiple parents. Both colocated and non-colocated Git-backed jj repositories are supported. Files above 16 MiB report an error and clear their indicators; request batches are limited to 64 MiB or 1024 files.
+1 -1
View File
@@ -4,7 +4,7 @@
Default controls and editing behavior. For custom bindings, see [configuration](configuration.md); for language-server commands, see [native LSP](configuration.md#native-lsp). Default controls and editing behavior. For custom bindings, see [configuration](configuration.md); for language-server commands, see [native LSP](configuration.md#native-lsp).
Ordinary documents show repository changes beside their hybrid line numbers by default: green `▐` for additions, red for removals, and yellow for modifications. [Configure `git_gutter`](configuration.md) to use `+` / `-` / `~`, colored numbers, or no change indicators. Bar/symbol gutters keep the order **number, marker, blank, text**; number-only modes use no extra column. Horizontal scrolling never moves indicators, and wrapped continuation rows do not repeat them. Ordinary documents show repository changes beside their hybrid line numbers by default: green `▌` for additions, red for removals, and yellow for modifications. [Configure `git_gutter`](configuration.md) to use `+` / `-` / `~`, colored numbers, or no change indicators. Bar/symbol gutters keep the order **number, blank, marker, blank, text**; number-only modes use no extra column. Horizontal scrolling never moves indicators, and wrapped continuation rows do not repeat them.
The baseline is Git `HEAD` or Git-backed jj's parent `@-`, so indicators include unsaved edits and remain after saving until the repository baseline changes. A deletion marks the next surviving source row; at EOF it marks the last editor row, including the final empty row after a trailing newline. An empty file anchors removals on row 0. Replacements mark paired rows modified and extra current rows added. Historical snapshots do not show live changes. Eligible named files not yet on disk show nonempty unsaved text as added; tracked files missing on disk still compare their buffer text against the baseline. Non-repository/ignored/binary/symlink files, files over 16 MiB, and jj changes with multiple parents have no indicators. The baseline is Git `HEAD` or Git-backed jj's parent `@-`, so indicators include unsaved edits and remain after saving until the repository baseline changes. A deletion marks the next surviving source row; at EOF it marks the last editor row, including the final empty row after a trailing newline. An empty file anchors removals on row 0. Replacements mark paired rows modified and extra current rows added. Historical snapshots do not show live changes. Eligible named files not yet on disk show nonempty unsaved text as added; tracked files missing on disk still compare their buffer text against the baseline. Non-repository/ignored/binary/symlink files, files over 16 MiB, and jj changes with multiple parents have no indicators.
+15 -12
View File
@@ -1515,7 +1515,7 @@ fn draw_document(
editor.gutter_style(), editor.gutter_style(),
crate::gutter::GutterStyle::Bar | crate::gutter::GutterStyle::Symbols crate::gutter::GutterStyle::Bar | crate::gutter::GutterStyle::Symbols
); );
let gutter_width = (digits + 1 + usize::from(marker_gutter)) as u16; let gutter_width = (digits + 1 + 2 * usize::from(marker_gutter)) as u16;
let [gutter, viewport] = Layout::horizontal([ let [gutter, viewport] = Layout::horizontal([
Constraint::Length(if viewport.width > gutter_width { Constraint::Length(if viewport.width > gutter_width {
gutter_width gutter_width
@@ -1787,8 +1787,9 @@ fn draw_document(
}; };
let mut spans = vec![Span::styled(format!("{number:>digits$}"), style)]; let mut spans = vec![Span::styled(format!("{number:>digits$}"), style)];
if marker_gutter { if marker_gutter {
spans.push(Span::raw(" "));
let marker = match (editor.gutter_style(), change) { let marker = match (editor.gutter_style(), change) {
(crate::gutter::GutterStyle::Bar, Some(_)) => "▐", (crate::gutter::GutterStyle::Bar, Some(_)) => "▌",
(_, Some(crate::gutter::Change::Added)) => "+", (_, Some(crate::gutter::Change::Added)) => "+",
(_, Some(crate::gutter::Change::Removed)) => "-", (_, Some(crate::gutter::Change::Removed)) => "-",
(_, Some(crate::gutter::Change::Modified)) => "~", (_, Some(crate::gutter::Change::Modified)) => "~",
@@ -14240,7 +14241,7 @@ end
], ],
)); ));
for (mode, marker) in [ for (mode, marker) in [
(GutterStyle::Bar, "▐"), (GutterStyle::Bar, "▌"),
(GutterStyle::Symbols, "+"), (GutterStyle::Symbols, "+"),
(GutterStyle::LineNumbers, ""), (GutterStyle::LineNumbers, ""),
(GutterStyle::None, ""), (GutterStyle::None, ""),
@@ -14253,20 +14254,22 @@ end
.unwrap(); .unwrap();
let viewport = views.panels.caret.unwrap().0; let viewport = views.panels.caret.unwrap().0;
let markers = matches!(mode, GutterStyle::Bar | GutterStyle::Symbols); let markers = matches!(mode, GutterStyle::Bar | GutterStyle::Symbols);
assert_eq!(viewport.x, if markers { 3 } else { 2 }); assert_eq!(viewport.x, if markers { 4 } else { 2 });
let buffer = terminal.backend().buffer(); let buffer = terminal.backend().buffer();
assert_eq!(buffer[(viewport.x - 1, viewport.y)].symbol(), " "); assert_eq!(buffer[(viewport.x - 1, viewport.y)].symbol(), " ");
if markers { if markers {
assert_eq!(buffer[(1, viewport.y)].symbol(), marker); assert_eq!(buffer[(1, viewport.y)].symbol(), " ");
assert_eq!(buffer[(2, viewport.y)].symbol(), marker);
for (offset, color) in [Color::Green, Color::Red, Color::Yellow] for (offset, color) in [Color::Green, Color::Red, Color::Yellow]
.into_iter() .into_iter()
.enumerate() .enumerate()
{ {
assert_eq!(buffer[(1, viewport.y + offset as u16)].fg, color); assert_eq!(buffer[(1, viewport.y + offset as u16)].symbol(), " ");
assert_eq!(buffer[(2, viewport.y + offset as u16)].fg, color);
} }
if mode == GutterStyle::Symbols { if mode == GutterStyle::Symbols {
assert_eq!(buffer[(1, viewport.y + 1)].symbol(), "-"); assert_eq!(buffer[(2, viewport.y + 1)].symbol(), "-");
assert_eq!(buffer[(1, viewport.y + 2)].symbol(), "~"); assert_eq!(buffer[(2, viewport.y + 2)].symbol(), "~");
} }
} else if mode == GutterStyle::LineNumbers { } else if mode == GutterStyle::LineNumbers {
assert_eq!(buffer[(0, viewport.y)].fg, Color::Cyan); assert_eq!(buffer[(0, viewport.y)].fg, Color::Cyan);
@@ -14286,10 +14289,10 @@ end
assert!(scrolled_views.offsets[&editor.buffer_id()].1 > 0); assert!(scrolled_views.offsets[&editor.buffer_id()].1 > 0);
let text_area = scrolled_views.panels.caret.unwrap().0; let text_area = scrolled_views.panels.caret.unwrap().0;
assert_eq!( assert_eq!(
scrolled_terminal.backend().buffer()[(1, text_area.y)].symbol(), scrolled_terminal.backend().buffer()[(2, text_area.y)].symbol(),
"+" "+"
); );
assert_eq!(text_area.x, 3); assert_eq!(text_area.x, 4);
editor.handle(Input::Home); editor.handle(Input::Home);
editor.execute("word_wrap.toggle"); editor.execute("word_wrap.toggle");
let mut views = BufferViewports::default(); let mut views = BufferViewports::default();
@@ -14324,11 +14327,11 @@ end
_ => " ", _ => " ",
}; };
assert_eq!( assert_eq!(
terminal.backend().buffer()[(1, viewport.y + screen as u16)].symbol(), terminal.backend().buffer()[(2, viewport.y + screen as u16)].symbol(),
expected expected
); );
} }
for width in [1, 2, 3] { for width in [1, 2, 3, 4] {
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(width, 3)).unwrap(); let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(width, 3)).unwrap();
terminal terminal
.draw(|frame| views.draw(frame, &mut editor)) .draw(|frame| views.draw(frame, &mut editor))