From bd786566914f7e1b0ad5e85e453e7ae9d538346d Mon Sep 17 00:00:00 2001 From: Valdemar Lorenzen Date: Thu, 8 Oct 2026 11:23:52 +0200 Subject: [PATCH] feat: show relative line numbers around primary cursor --- README.md | 2 +- src/tui.rs | 29 +++++++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 1daea93..572de99 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ The terminal adapter owns raw mode, alternate-screen state, bracketed paste, and The TUI uses Ratatui constraint-based layout for a flexible buffer viewport, a one-row status bar, and a one-row prompt/message area. Scrolling, clipping, highlights, and cursor placement use the allocated regions, not terminal-wide offsets. Below three rows the status bar is hidden; at one row the buffer remains visible unless an active command/search prompt takes priority. Zero-sized terminals render nothing. Layout and terminal-cell text mapping remain frontend-owned. -The buffer viewport includes a fixed left gutter with right-aligned, one-based absolute line numbers and a highlighted primary cursor line number. Gutter width follows the total line count plus one separator cell, including an empty final row after a trailing newline. Horizontal scrolling affects only text; search/selection highlights and carets never paint over the gutter. Rows beyond the buffer have no number. 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. Gutter width follows the total line count plus one separator cell, including an empty final row after a trailing newline. Horizontal scrolling affects only text; search/selection highlights and carets never paint over the gutter. Rows beyond the buffer have no number. The gutter is hidden when it would leave no text column. Built-in Ex commands register as the protected `core` feature in the same command registry used by plugins. External plugins cannot claim core status, replace existing commands, or disable core. User configuration can override any keybinding without replacing the core implementation. `:plugins` lists active features and frontend capabilities. diff --git a/src/tui.rs b/src/tui.rs index a074df8..bc415b1 100644 --- a/src/tui.rs +++ b/src/tui.rs @@ -246,7 +246,12 @@ fn draw(frame: &mut Frame, editor: &Editor, top: &mut usize, left: &mut usize) { } else { Style::default().fg(Color::DarkGray) }; - Line::styled(format!("{:>digits$} ", row + 1), style) + let number = if row == cursor.row { + row + 1 + } else { + row.abs_diff(cursor.row) + }; + Line::styled(format!("{number:>digits$} "), style) }) .collect(); frame.render_widget(Paragraph::new(numbers), gutter); @@ -509,7 +514,7 @@ mod tests { .unwrap(); assert_eq!((top, left), (8, 8)); let buffer = terminal.backend().buffer(); - for (y, expected) in [(2, " 9 "), (3, "10 "), (4, "11 ")] { + for (y, expected) in [(2, " 2 "), (3, " 1 "), (4, "11 ")] { let number: String = (3..6).map(|x| buffer[(x, y)].symbol()).collect(); assert_eq!(number, expected); assert_eq!(buffer[(6, y)].symbol(), "8"); @@ -533,6 +538,26 @@ mod tests { terminal.get_cursor_position().unwrap(), ratatui::layout::Position::new(6, 4) ); + editor.handle(Input::Up); + editor.handle(Input::Up); + terminal + .draw(|frame| draw(frame, &editor, &mut top, &mut left)) + .unwrap(); + let buffer = terminal.backend().buffer(); + for (y, expected) in [(2, "10 "), (3, " 1 "), (4, " 2 ")] { + let number: String = (3..6).map(|x| buffer[(x, y)].symbol()).collect(); + assert_eq!(number, expected); + } + editor.handle(Input::Ctrl('e')); + assert_eq!(editor.selection_count(), 2); + terminal + .draw(|frame| draw(frame, &editor, &mut top, &mut left)) + .unwrap(); + let buffer = terminal.backend().buffer(); + for (y, expected) in [(2, " 1 "), (3, "11 "), (4, " 1 ")] { + let number: String = (3..6).map(|x| buffer[(x, y)].symbol()).collect(); + assert_eq!(number, expected); + } } #[test]