fix: show revision-aware blame in diff buffers
Diff and snapshot sources were excluded before scheduling, and the separate diff renderer never painted attribution. Capture per-side Git provenance, preserve private object stores for historical tabs, and reuse the current-line worker and cache.
This commit is contained in:
+227
-5
@@ -2593,6 +2593,7 @@ fn diff_comparison(files: Vec<ChangedFile>) -> DiffState {
|
||||
diff.apply_result(
|
||||
id,
|
||||
Ok(crate::diff::Outcome::Comparison(crate::diff::Comparison {
|
||||
blame: Default::default(),
|
||||
root: std::path::PathBuf::new(),
|
||||
editable_paths: Default::default(),
|
||||
before: "old revision".into(),
|
||||
@@ -4805,6 +4806,7 @@ fn immutable_diff_draw_path_preserves_document_until_explicit_close() {
|
||||
editor.apply_diff_result(
|
||||
id,
|
||||
Ok(crate::diff::Outcome::Comparison(crate::diff::Comparison {
|
||||
blame: Default::default(),
|
||||
root: std::path::PathBuf::new(),
|
||||
editable_paths: Default::default(),
|
||||
before: "old revision".into(),
|
||||
@@ -5652,6 +5654,7 @@ fn comparison_titles_follow_loaded_counts_and_each_tabs_context() {
|
||||
editor.apply_diff_result(
|
||||
id,
|
||||
Ok(crate::diff::Outcome::Comparison(crate::diff::Comparison {
|
||||
blame: Default::default(),
|
||||
root: directory.0.clone(),
|
||||
editable_paths: Default::default(),
|
||||
before: "baseline".into(),
|
||||
@@ -5745,6 +5748,7 @@ fn comparison_title_counts_follow_live_edits_and_undo_in_hidden_tabs() {
|
||||
editor.apply_diff_result(
|
||||
id,
|
||||
Ok(crate::diff::Outcome::Comparison(crate::diff::Comparison {
|
||||
blame: Default::default(),
|
||||
root: directory.0.clone(),
|
||||
editable_paths: ["live.rs".into()].into(),
|
||||
before: "baseline".into(),
|
||||
@@ -6377,6 +6381,7 @@ fn both_bars_bound_palettes_diff_selectors_docks_and_diff_prompts() {
|
||||
editor.apply_diff_result(
|
||||
id,
|
||||
Ok(crate::diff::Outcome::Comparison(crate::diff::Comparison {
|
||||
blame: Default::default(),
|
||||
root: directory.0.clone(),
|
||||
editable_paths: Default::default(),
|
||||
before: "baseline".into(),
|
||||
@@ -6455,6 +6460,218 @@ fn narrow_tab_bar_keeps_the_active_title_visible_and_handles_empty_areas() {
|
||||
assert!(titles.contains("2 [No Name]"), "{titles}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inline_blame_is_visible_in_focused_diff_buffers() {
|
||||
for unified in [false, true] {
|
||||
for before in [false, true] {
|
||||
let mut editor = readonly_diff_editor(deletion_file());
|
||||
if unified {
|
||||
editor.execute("diff.view.toggle");
|
||||
}
|
||||
editor.handle(Input::Char('\t'));
|
||||
if editor.diff_buffer_focus() != Some(if before { Focus::Before } else { Focus::After })
|
||||
{
|
||||
editor.handle(Input::Char('\t'));
|
||||
}
|
||||
let source = editor.blame_source().unwrap();
|
||||
let blame = Arc::new(crate::blame::LineBlame {
|
||||
commit: "0123456789abcdef".into(),
|
||||
author: "Author".into(),
|
||||
summary: "Original".into(),
|
||||
uncommitted: false,
|
||||
});
|
||||
editor.set_line_blame(source.clone(), blame.clone());
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(180, 12)).unwrap();
|
||||
let mut views = BufferViewports::default();
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
let screen = screen_text(&terminal);
|
||||
assert!(screen.contains("Author · Original (01234567)"), "{screen}");
|
||||
let caret = terminal.get_cursor_position().unwrap();
|
||||
assert!(pane_screen(&terminal, Rect::new(0, caret.y, 180, 1)).contains("Author"));
|
||||
editor.handle(Input::Char('\t'));
|
||||
assert!(!editor.set_line_blame(source, blame));
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
assert!(!screen_text(&terminal).contains("Author · Original"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_blame_uses_final_wrap_and_survives_a_historical_tab() {
|
||||
for unified in [false, true] {
|
||||
let text = "x".repeat(200);
|
||||
let mut editor = readonly_diff_editor(syntax_diff_file("file.txt", &text, "after"));
|
||||
if unified {
|
||||
editor.execute("diff.view.toggle");
|
||||
}
|
||||
editor.handle(Input::Char('\t'));
|
||||
editor.execute("word_wrap.toggle");
|
||||
let source = editor.blame_source().unwrap();
|
||||
let blame = Arc::new(crate::blame::LineBlame {
|
||||
commit: String::new(),
|
||||
author: String::new(),
|
||||
summary: String::new(),
|
||||
uncommitted: true,
|
||||
});
|
||||
editor.set_line_blame(source.clone(), blame.clone());
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(180, 14)).unwrap();
|
||||
let mut views = BufferViewports::default();
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
let caret = terminal.get_cursor_position().unwrap();
|
||||
let screen = screen_text(&terminal);
|
||||
assert_eq!(screen.matches("Uncommitted").count(), 1, "{screen}");
|
||||
assert!(!pane_screen(&terminal, Rect::new(0, caret.y, 180, 1)).contains("Uncommitted"));
|
||||
let mut narrow = Terminal::new(ratatui::backend::TestBackend::new(12, 14)).unwrap();
|
||||
narrow.draw(|frame| views.draw(frame, &mut editor)).unwrap();
|
||||
assert!(!screen_text(&narrow).contains("Uncommitted"));
|
||||
editor.execute("tab.new");
|
||||
assert!(editor.diff().is_none());
|
||||
if !editor.word_wrap() {
|
||||
editor.execute("word_wrap.toggle");
|
||||
}
|
||||
editor.set_cached_line_blame(&source, blame);
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
assert!(screen_text(&terminal).contains("Uncommitted"));
|
||||
editor.handle(Input::Char('i'));
|
||||
editor.handle(Input::Char('z'));
|
||||
assert_eq!(editor.lines().join("\n"), text);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inline_blame_loads_real_historical_and_working_diff_sources() {
|
||||
let directory = Directory::new();
|
||||
let git = |args: &[&str]| {
|
||||
let output = std::process::Command::new("git")
|
||||
.current_dir(&directory.0)
|
||||
.args(args)
|
||||
.env("GIT_CONFIG_GLOBAL", "/dev/null")
|
||||
.env("GIT_CONFIG_NOSYSTEM", "1")
|
||||
.env("GIT_AUTHOR_NAME", "Author")
|
||||
.env("GIT_AUTHOR_EMAIL", "author@example.test")
|
||||
.env("GIT_COMMITTER_NAME", "Author")
|
||||
.env("GIT_COMMITTER_EMAIL", "author@example.test")
|
||||
.env("GIT_AUTHOR_DATE", "2001-01-01T00:00:00Z")
|
||||
.env("GIT_COMMITTER_DATE", "2001-01-01T00:00:00Z")
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
String::from_utf8(output.stdout).unwrap().trim().to_owned()
|
||||
};
|
||||
git(&["init", "-q", "--template="]);
|
||||
let path = directory.file("file.txt", "original\n");
|
||||
git(&["add", "file.txt"]);
|
||||
git(&["commit", "-qm", "Original"]);
|
||||
let original = git(&["rev-parse", "HEAD"]);
|
||||
directory.file("file.txt", "historical\n");
|
||||
git(&["commit", "-qam", "Historical"]);
|
||||
let historical = git(&["rev-parse", "HEAD"]);
|
||||
git(&["rm", "-q", "file.txt"]);
|
||||
git(&["commit", "-qm", "Deleted"]);
|
||||
let config = directory.file(
|
||||
".git/vamp-config.rb",
|
||||
"Vamp.configure { |c| c.git_gutter = 'none' }",
|
||||
);
|
||||
let mut session = EditorSession::open(Some(path.clone()), vec![], Some(&config)).unwrap();
|
||||
let ready = |session: &mut EditorSession, predicate: &dyn Fn(&Editor) -> bool| {
|
||||
let deadline = std::time::Instant::now() + Duration::from_secs(10);
|
||||
while !predicate(session.editor()) {
|
||||
assert!(
|
||||
std::time::Instant::now() < deadline,
|
||||
"message={:?}, state={:?}, blame={:?}, text={:?}",
|
||||
session.editor().message(),
|
||||
session.editor().diff().map(|diff| (
|
||||
diff.phase,
|
||||
diff.focus,
|
||||
diff.file,
|
||||
&diff.error
|
||||
)),
|
||||
session.editor().line_blame(),
|
||||
session.editor().lines()
|
||||
);
|
||||
session.poll();
|
||||
std::thread::sleep(Duration::from_millis(1));
|
||||
}
|
||||
};
|
||||
session.execute("diff");
|
||||
ready(&mut session, &|editor| {
|
||||
editor.diff().is_some_and(|diff| diff.catalog.is_some())
|
||||
});
|
||||
for revision in [&original, &historical] {
|
||||
for ch in revision.chars() {
|
||||
session.handle(Input::Char(ch));
|
||||
}
|
||||
session.handle(Input::Down);
|
||||
session.handle(Input::Enter);
|
||||
}
|
||||
ready(&mut session, &|editor| {
|
||||
editor
|
||||
.diff()
|
||||
.is_some_and(|diff| diff.selected_file_loaded())
|
||||
});
|
||||
session.handle(Input::Char('\t'));
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(180, 12)).unwrap();
|
||||
let mut views = BufferViewports::default();
|
||||
for expected in ["Original", "Historical"] {
|
||||
ready(&mut session, &|editor| {
|
||||
editor
|
||||
.line_blame()
|
||||
.is_some_and(|label| label.contains(expected))
|
||||
});
|
||||
terminal
|
||||
.draw(|frame| views.draw_session(frame, &mut session))
|
||||
.unwrap();
|
||||
let screen = screen_text(&terminal);
|
||||
assert!(screen.contains(&format!("Author · {expected}")), "{screen}");
|
||||
assert!(!screen.contains("Uncommitted"), "{screen}");
|
||||
session.handle(Input::Char('\t'));
|
||||
}
|
||||
assert!(!path.exists());
|
||||
session.execute("diff.close");
|
||||
directory.file("file.txt", "restored\n");
|
||||
git(&["add", "file.txt"]);
|
||||
git(&["commit", "-qm", "Restored"]);
|
||||
directory.file("file.txt", "unsaved baseline\n");
|
||||
session.editor_mut().refresh_files();
|
||||
session.execute("diff");
|
||||
ready(&mut session, &|editor| {
|
||||
editor.diff().is_some_and(|diff| diff.catalog.is_some())
|
||||
});
|
||||
for ch in git(&["rev-parse", "HEAD"]).chars() {
|
||||
session.handle(Input::Char(ch));
|
||||
}
|
||||
session.handle(Input::Down);
|
||||
session.handle(Input::Enter);
|
||||
session.handle(Input::Enter);
|
||||
ready(&mut session, &|editor| {
|
||||
editor
|
||||
.diff()
|
||||
.is_some_and(|diff| diff.selected_file_loaded())
|
||||
});
|
||||
session.handle(Input::Char('\t'));
|
||||
session.handle(Input::Char('\t'));
|
||||
ready(&mut session, &|editor| {
|
||||
editor.line_blame() == Some("Uncommitted")
|
||||
});
|
||||
terminal
|
||||
.draw(|frame| views.draw_session(frame, &mut session))
|
||||
.unwrap();
|
||||
assert!(screen_text(&terminal).contains("Uncommitted"));
|
||||
assert_eq!(fs::read_to_string(path).unwrap(), "unsaved baseline\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inline_blame_sanitizes_metadata_preserves_text_and_hides_stale_labels() {
|
||||
let directory = Directory::new();
|
||||
@@ -6545,11 +6762,6 @@ fn inline_blame_uses_last_wrap_continuation_and_never_overflows_narrow_panes() {
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
assert!(!screen_text(&terminal).contains("Uncommitted"));
|
||||
let mut historical = readonly_diff_editor(deletion_file());
|
||||
assert!(historical.blame_source().is_none());
|
||||
historical.execute("tab.new");
|
||||
assert!(historical.diff().is_none());
|
||||
assert!(historical.blame_source().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -7782,6 +7994,7 @@ fn deferred_lsp_definition_centers_only_successful_current_diff_results() {
|
||||
editor.apply_diff_result(
|
||||
id,
|
||||
Ok(crate::diff::Outcome::Comparison(crate::diff::Comparison {
|
||||
blame: Default::default(),
|
||||
root: directory.0.clone(),
|
||||
editable_paths: Default::default(),
|
||||
before: "baseline".into(),
|
||||
@@ -7880,6 +8093,13 @@ fn install_readonly_diff(editor: &mut Editor, file: ChangedFile) {
|
||||
editor.apply_diff_result(
|
||||
id,
|
||||
Ok(crate::diff::Outcome::Comparison(crate::diff::Comparison {
|
||||
blame: std::array::from_fn(|_| {
|
||||
Some(crate::diff::BlameRevision {
|
||||
store: editor.project_root().join(".git"),
|
||||
commit: "0123456789abcdef0123456789abcdef01234567".into(),
|
||||
keepalive: None,
|
||||
})
|
||||
}),
|
||||
root: editor.project_root().to_path_buf(),
|
||||
editable_paths: Default::default(),
|
||||
before: "baseline".into(),
|
||||
@@ -8732,6 +8952,7 @@ fn diff_semantic_colors_stay_buffer_local_across_focus_changes() {
|
||||
editor.apply_diff_result(
|
||||
id,
|
||||
Ok(crate::diff::Outcome::Comparison(crate::diff::Comparison {
|
||||
blame: Default::default(),
|
||||
root: directory.0.clone(),
|
||||
editable_paths: if writable {
|
||||
[std::path::PathBuf::from("live.rs")].into()
|
||||
@@ -8862,6 +9083,7 @@ fn editable_after_renders_live_syntax_caret_visual_selection_and_command_prompt(
|
||||
editor.apply_diff_result(
|
||||
id,
|
||||
Ok(crate::diff::Outcome::Comparison(crate::diff::Comparison {
|
||||
blame: Default::default(),
|
||||
root: directory.0.clone(),
|
||||
editable_paths: [std::path::PathBuf::from("live.rs")].into(),
|
||||
before: "baseline".into(),
|
||||
|
||||
Reference in New Issue
Block a user