fix: clip inline blame instead of hiding long metadata

The full-label width check hid current-line attribution when only part of the commit metadata fit. Render into the remaining cells without covering source text; preserve offscreen and no-space guards.
This commit is contained in:
Valdemar Lorenzen
2026-10-20 12:06:54 +02:00
parent 4fb1be7972
commit eae3bdba16
5 changed files with 49 additions and 3 deletions
+2
View File
@@ -1,6 +1,8 @@
- [x] improve: checkmark persistance should be preserved in a diff where a certain file hasn't changed. - [x] improve: checkmark persistance should be preserved in a diff where a certain file hasn't changed.
- this is to avoid that changing a single file will cause all reviewed files to drop the reviewed state, making it harder to spot what actually requires a re-review and what can be ignored. - this is to avoid that changing a single file will cause all reviewed files to drop the reviewed state, making it harder to spot what actually requires a re-review and what can be ignored.
- [x] bug: diff view - git blame doesn't show in diff buffers for some reason. - [x] bug: diff view - git blame doesn't show in diff buffers for some reason.
- [ ] feature: diff view - command to copy all review comments in the diff to the configured clipboard.
- format should be the same as when printed via the cli
- [ ] feature: diff view - GitHub code review integration after the local review UX is usable. - [ ] feature: diff view - GitHub code review integration after the local review UX is usable.
- publish local reviews explicitly: general PR comments, inline line/range comments, review summaries, approval, and requests for changes. - publish local reviews explicitly: general PR comments, inline line/range comments, review summaries, approval, and requests for changes.
- preserve explicit PR identity and reviewed commit IDs; validate anchors against GitHub's PR diff and recheck the PR head before submission. - preserve explicit PR identity and reviewed commit IDs; validate anchors against GitHub's PR diff and recheck the PR head before submission.
+1
View File
@@ -91,6 +91,7 @@ Shared-workspace parity checks compare ordinary and resolver diff geometry/conte
Repository gutter checks cover LF/CRLF normalization, additions/replacements/deletion anchors, staged/disk/unsaved Git text, unborn and missing named files, ignored/unsupported paths, read-only colocated/non-colocated jj queries, stale results and per-buffer isolation, plus rendered modes, colors, current-line precedence, wrap/scroll and narrow/split geometry. Live PTY smoke verifies all four modes, edit/undo refresh, external Git HEAD changes, and jj parent changes without editor restart. Repository gutter checks cover LF/CRLF normalization, additions/replacements/deletion anchors, staged/disk/unsaved Git text, unborn and missing named files, ignored/unsupported paths, read-only colocated/non-colocated jj queries, stale results and per-buffer isolation, plus rendered modes, colors, current-line precedence, wrap/scroll and narrow/split geometry. Live PTY smoke verifies all four modes, edit/undo refresh, external Git HEAD changes, and jj parent changes without editor restart.
Blame checks cover selected historical commits rather than live HEAD, renamed/deleted paths, unsafe/nonregular sources, Git-backed non-colocated jj, and privately fetched objects surviving comparison/repository teardown until the final snapshot releases them. Render/session regressions cover both split/unified sides, stale side results, wrapping, narrow panes, detached historical tabs, and Working attribution. Live PTY smoke opens both historical sides and unified view after deleting the file from the checkout, observes the correct commit labels, opens a historical tab, and exits with unchanged HEAD and tracked state. Blame checks cover selected historical commits rather than live HEAD, renamed/deleted paths, unsafe/nonregular sources, Git-backed non-colocated jj, and privately fetched objects surviving comparison/repository teardown until the final snapshot releases them. Render/session regressions cover both split/unified sides, stale side results, wrapping, narrow panes, detached historical tabs, and Working attribution. Live PTY smoke opens both historical sides and unified view after deleting the file from the checkout, observes the correct commit labels, opens a historical tab, and exits with unchanged HEAD and tracked state.
Partial-label checks navigate short and longer source lines with wide/combining author names and long commit summaries in ordinary, split, and unified views. Live PTY smoke repeats three-line navigation in split/unified views with a long summary, observing attribution on every source line with unchanged source bytes and HEAD.
Normal-picker checks cover staged/disk/unsaved status, binary and mode changes, exact and ambiguous renames, deleted paths, unborn repositories, aliases and unusual filenames, ignores and safe roots, linked worktrees, and read-only colocated/non-colocated/conflicted jj parents. Catalog/render checks retain unchanged rows, query/selection/expansion and Tree/List navigation, clear stale metadata, restore recreated files, and sanitize rename labels. Live PTY smoke checks A/M/D/R colors in floating and docked pickers with the line gutter disabled, Git HEAD/jj parent refresh, and unchanged jj operation state during polling. Normal-picker checks cover staged/disk/unsaved status, binary and mode changes, exact and ambiguous renames, deleted paths, unborn repositories, aliases and unusual filenames, ignores and safe roots, linked worktrees, and read-only colocated/non-colocated/conflicted jj parents. Catalog/render checks retain unchanged rows, query/selection/expansion and Tree/List navigation, clear stale metadata, restore recreated files, and sanitize rename labels. Live PTY smoke checks A/M/D/R colors in floating and docked pickers with the line gutter disabled, Git HEAD/jj parent refresh, and unchanged jj operation state during polling.
+1 -1
View File
@@ -260,7 +260,7 @@ Text wraps at word boundaries where possible, with grapheme-safe breaks for long
Inline blame is enabled by default in live file buffers and on the focused side of split/unified diffs. The primary caret's line shows its author, commit summary, and short commit ID after the document text; changed or newly inserted tracked-file lines show **Uncommitted**, including unsaved edits. Live Git-backed jj buffers use the working-copy revision's single parent, not a potentially stale Git HEAD. Merge working copies with multiple parents omit live attribution. Inline blame is enabled by default in live file buffers and on the focused side of split/unified diffs. The primary caret's line shows its author, commit summary, and short commit ID after the document text; changed or newly inserted tracked-file lines show **Uncommitted**, including unsaved edits. Live Git-backed jj buffers use the working-copy revision's single parent, not a potentially stale Git HEAD. Merge working copies with multiple parents omit live attribution.
Loading happens in the background without network access or changing repository refs/checkouts. Attribution is loaded once for the active buffer's in-memory contents; navigation then selects cached lines immediately, and movement during a cold load does not restart it. Editing, saving, reloading, changing source buffers, regaining terminal focus, or toggling blame invalidates the cached attribution. Historical sides use their captured commit and side-specific path, including renamed or deleted files no longer on disk; historical tabs retain that source and any privately fetched objects after the comparison closes. Frozen Working sides use their captured parent baseline rather than a later checkout. Missing, untracked, unborn, and non-repository live files have no indicator. The indicator is hidden when the caret is offscreen or the full label cannot fit; wrapped lines use their final visible continuation without covering text, selections, or the caret. Popups retain their normal layering. Loading happens in the background without network access or changing repository refs/checkouts. Attribution is loaded once for the active buffer's in-memory contents; navigation then selects cached lines immediately, and movement during a cold load does not restart it. Editing, saving, reloading, changing source buffers, regaining terminal focus, or toggling blame invalidates the cached attribution. Historical sides use their captured commit and side-specific path, including renamed or deleted files no longer on disk; historical tabs retain that source and any privately fetched objects after the comparison closes. Frozen Working sides use their captured parent baseline rather than a later checkout. Missing, untracked, unborn, and non-repository live files have no indicator. Long attribution labels clip to the spare cells after the source text instead of disappearing entirely. The indicator is hidden when the caret is offscreen or no inline space remains; wrapped lines use their final visible continuation without covering text, selections, or the caret. Popups retain their normal layering.
Use `:git_blame.toggle`, the palette action, or remappable `config.core.git_blame.toggle`; there is no default keybinding. Set `config.git_blame = false` to disable it at startup. Disabling stops the worker and clears the label without changing text or undo history. Use `:git_blame.toggle`, the palette action, or remappable `config.core.git_blame.toggle`; there is no default keybinding. Set `config.git_blame = false` to disable it at startup. Disabling stops the worker and clears the label without changing text or undo history.
+3 -2
View File
@@ -3230,8 +3230,9 @@ fn paint_editor_line(
fn paint_inline_blame(frame: &mut Frame, label: &str, line: &str, area: Rect, left: usize) { fn paint_inline_blame(frame: &mut Frame, label: &str, line: &str, area: Rect, left: usize) {
let end = display_column(line, usize::MAX); let end = display_column(line, usize::MAX);
let start = end.saturating_sub(left).saturating_add(2); let start = end.saturating_sub(left).saturating_add(2);
let label_width = UnicodeWidthStr::width(label); let label_width =
if end >= left && start.saturating_add(label_width) <= usize::from(area.width) { UnicodeWidthStr::width(label).min(usize::from(area.width).saturating_sub(start));
if end >= left && label_width > 0 {
frame.render_widget( frame.render_widget(
Paragraph::new(label).style(Style::default().fg(Color::DarkGray)), Paragraph::new(label).style(Style::default().fg(Color::DarkGray)),
Rect::new(area.x + start as u16, area.y, label_width as u16, 1), Rect::new(area.x + start as u16, area.y, label_width as u16, 1),
+42
View File
@@ -6500,6 +6500,48 @@ fn inline_blame_is_visible_in_focused_diff_buffers() {
} }
} }
#[test]
fn inline_blame_remains_visible_on_lines_with_room_for_partial_metadata() {
let directory = Directory::new();
for (context, width) in [("document", 80), ("split", 180), ("unified", 120)] {
let text = "short\nlet meaningful_identifier = call_with_arguments();\ntail";
let mut editor = if context == "document" {
Editor::open(Some(directory.file("file.txt", text)), vec![]).unwrap()
} else {
let mut editor = readonly_diff_editor(syntax_diff_file("file.txt", text, "changed"));
if context == "unified" {
editor.execute("diff.view.toggle");
}
editor.handle(Input::Char('\t'));
editor
};
let source = editor.blame_source().unwrap();
let blame = Arc::new(crate::blame::LineBlame {
commit: "0123456789abcdef".into(),
author: "界 e\u{301} Author".into(),
summary: "A detailed commit summary describing the source change".into(),
uncommitted: false,
});
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(width, 12)).unwrap();
let mut views = BufferViewports::default();
for row in 0..3 {
assert_eq!(editor.cursor().row, row);
editor.set_cached_line_blame(&source, blame.clone());
terminal
.draw(|frame| views.draw(frame, &mut editor))
.unwrap();
let caret = terminal.get_cursor_position().unwrap();
let line = pane_screen(&terminal, Rect::new(0, caret.y, width, 1));
assert!(
line.contains("Author") && line.contains("e\u{301}"),
"{context}, source row={row}: {line}"
);
assert!(line.contains(editor.lines()[row].as_str()), "{line}");
editor.handle(Input::Down);
}
}
}
#[test] #[test]
fn diff_blame_uses_final_wrap_and_survives_a_historical_tab() { fn diff_blame_uses_final_wrap_and_survives_a_historical_tab() {
for unified in [false, true] { for unified in [false, true] {