perf: cache file attribution for instant blame navigation
This commit is contained in:
+168
-26
@@ -176,6 +176,7 @@ pub struct EditorSession {
|
||||
diff_workers: BTreeMap<usize, DiffWorker>,
|
||||
blame_worker: Option<BlameWorker>,
|
||||
blame_source: Option<BlameSource>,
|
||||
blame_lines: Option<Vec<Option<Arc<crate::blame::LineBlame>>>>,
|
||||
blame_request_id: u64,
|
||||
battery: Option<crate::battery::Battery>,
|
||||
battery_enabled: bool,
|
||||
@@ -281,6 +282,7 @@ impl EditorSession {
|
||||
diff_workers: BTreeMap::new(),
|
||||
blame_worker: None,
|
||||
blame_source: None,
|
||||
blame_lines: None,
|
||||
blame_request_id: 0,
|
||||
battery: None,
|
||||
battery_enabled: false,
|
||||
@@ -1027,10 +1029,11 @@ impl EditorSession {
|
||||
if !self
|
||||
.blame_source
|
||||
.as_ref()
|
||||
.is_some_and(|source| self.editor.blame_source_is_current(source))
|
||||
.is_some_and(|source| self.editor.blame_file_source_is_current(source))
|
||||
{
|
||||
self.blame_source = self.editor.blame_source();
|
||||
self.blame_request_id = self.blame_request_id.wrapping_add(1);
|
||||
self.blame_lines = None;
|
||||
changed |= self.editor.clear_line_blame();
|
||||
if let Some(source) = &self.blame_source {
|
||||
if self.blame_worker.is_none() {
|
||||
@@ -1040,9 +1043,8 @@ impl EditorSession {
|
||||
&& worker
|
||||
.request(
|
||||
self.blame_request_id,
|
||||
source.path.clone(),
|
||||
source.path.to_path_buf(),
|
||||
self.editor.lines().join("\n"),
|
||||
source.row,
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
@@ -1055,21 +1057,44 @@ impl EditorSession {
|
||||
while let Some((id, result)) = self.blame_worker.as_mut().and_then(BlameWorker::poll) {
|
||||
changed |= self.apply_blame_result(id, result);
|
||||
}
|
||||
changed
|
||||
changed | self.refresh_line_blame()
|
||||
}
|
||||
|
||||
fn apply_blame_result(
|
||||
&mut self,
|
||||
id: u64,
|
||||
result: Result<Option<crate::blame::LineBlame>, String>,
|
||||
result: Result<Option<Vec<Option<Arc<crate::blame::LineBlame>>>>, String>,
|
||||
) -> bool {
|
||||
if id == self.blame_request_id
|
||||
&& let Some(source) = &self.blame_source
|
||||
&& let Ok(Some(blame)) = result
|
||||
if id != self.blame_request_id
|
||||
|| !self
|
||||
.blame_source
|
||||
.as_ref()
|
||||
.is_some_and(|source| self.editor.blame_file_source_is_current(source))
|
||||
{
|
||||
return self.editor.set_line_blame(source.clone(), blame);
|
||||
return false;
|
||||
}
|
||||
false
|
||||
self.blame_lines = result.ok().flatten();
|
||||
self.refresh_line_blame()
|
||||
}
|
||||
|
||||
fn refresh_line_blame(&mut self) -> bool {
|
||||
if let Some(source) = &self.blame_source
|
||||
&& self.editor.blame_file_source_is_current(source)
|
||||
&& let Some(blame) = self
|
||||
.blame_lines
|
||||
.as_ref()
|
||||
.and_then(|lines| lines.get(self.editor.cursor().row))
|
||||
.and_then(Option::as_ref)
|
||||
{
|
||||
self.editor.set_cached_line_blame(source, blame.clone())
|
||||
} else {
|
||||
self.editor.clear_line_blame()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn refresh_blame(&mut self) -> bool {
|
||||
self.blame_source = None;
|
||||
self.poll_blame()
|
||||
}
|
||||
|
||||
fn poll_battery(&mut self) -> bool {
|
||||
@@ -1190,37 +1215,68 @@ mod tests {
|
||||
#[test]
|
||||
fn blame_sources_are_deduplicated_and_stale_results_cannot_reappear() {
|
||||
let fixture = Fixture::new();
|
||||
let path = fixture.file("source.txt", "first\nsecond");
|
||||
let path = fixture.file("source.txt", "first\nsecond\nthird");
|
||||
let mut session = EditorSession::open(Some(path.clone()), vec![], None).unwrap();
|
||||
assert!(session.editor.git_blame());
|
||||
let first = session.blame_request_id;
|
||||
session.poll_blame();
|
||||
assert_eq!(session.blame_request_id, first);
|
||||
let original = Arc::new(crate::blame::LineBlame {
|
||||
commit: "0123456789".into(),
|
||||
author: "Author".into(),
|
||||
summary: "Original".into(),
|
||||
uncommitted: false,
|
||||
});
|
||||
let second = Arc::new(crate::blame::LineBlame {
|
||||
commit: "0123456789".into(),
|
||||
author: "Author".into(),
|
||||
summary: "Second".into(),
|
||||
uncommitted: false,
|
||||
});
|
||||
let blame = || {
|
||||
Ok(Some(crate::blame::LineBlame {
|
||||
commit: "0123456789".into(),
|
||||
author: "Author".into(),
|
||||
summary: "Original".into(),
|
||||
uncommitted: false,
|
||||
}))
|
||||
Ok(Some(vec![
|
||||
Some(original.clone()),
|
||||
Some(second.clone()),
|
||||
Some(second.clone()),
|
||||
]))
|
||||
};
|
||||
session.handle(Input::Down);
|
||||
assert_eq!(session.blame_request_id, first);
|
||||
session.blame_worker = None;
|
||||
assert!(session.apply_blame_result(first, blame()));
|
||||
assert_eq!(
|
||||
session.editor.line_blame(),
|
||||
Some("Author · Second (01234567)")
|
||||
);
|
||||
let label = session.editor.line_blame().unwrap().as_ptr();
|
||||
session.handle(Input::Down);
|
||||
assert_eq!(session.editor.line_blame().unwrap().as_ptr(), label);
|
||||
assert!(!session.poll_blame());
|
||||
session.handle(Input::Up);
|
||||
session.handle(Input::Up);
|
||||
assert_eq!(
|
||||
session.editor.line_blame(),
|
||||
Some("Author · Original (01234567)")
|
||||
);
|
||||
session.handle(Input::Down);
|
||||
assert_eq!(session.blame_request_id, first);
|
||||
session.execute("tab.new");
|
||||
assert_eq!(session.blame_request_id, first);
|
||||
assert_eq!(
|
||||
session.editor.line_blame(),
|
||||
Some("Author · Original (01234567)")
|
||||
);
|
||||
let other = fixture.file("other.txt", "other");
|
||||
session.execute(&format!("edit {}", other.display()));
|
||||
assert!(session.editor.line_blame().is_none());
|
||||
session.handle(Input::Up);
|
||||
assert!(!session.apply_blame_result(first, blame()));
|
||||
session.execute("buffer 1");
|
||||
assert!(!session.apply_blame_result(first, blame()));
|
||||
let source = session.editor.blame_source().unwrap();
|
||||
let before_edit = session.blame_request_id;
|
||||
session.handle(Input::Char('i'));
|
||||
session.handle(Input::Char('x'));
|
||||
assert!(
|
||||
!session
|
||||
.editor
|
||||
.set_line_blame(source, blame().unwrap().unwrap())
|
||||
);
|
||||
assert_ne!(session.blame_request_id, before_edit);
|
||||
assert!(!session.apply_blame_result(before_edit, blame()));
|
||||
assert!(!session.editor.set_line_blame(source, original.clone()));
|
||||
assert!(session.editor.line_blame().is_none());
|
||||
session.handle(Input::Escape);
|
||||
session.execute("git_blame.toggle");
|
||||
assert!(!session.editor.git_blame());
|
||||
@@ -1241,6 +1297,92 @@ mod tests {
|
||||
assert!(disabled.blame_worker.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blame_navigation_uses_one_real_file_result_and_refreshes_its_baseline() {
|
||||
let fixture = Fixture::new();
|
||||
let path = fixture.file("source.txt", "first\nsecond\n");
|
||||
let git = |args: &[&str]| {
|
||||
let output = std::process::Command::new("git")
|
||||
.args(args)
|
||||
.current_dir(&fixture.0)
|
||||
.env("GIT_CONFIG_NOSYSTEM", "1")
|
||||
.env("GIT_CONFIG_GLOBAL", "/dev/null")
|
||||
.env("GIT_AUTHOR_NAME", "Author")
|
||||
.env("GIT_AUTHOR_EMAIL", "author@example.com")
|
||||
.env("GIT_COMMITTER_NAME", "Author")
|
||||
.env("GIT_COMMITTER_EMAIL", "author@example.com")
|
||||
.env("GIT_AUTHOR_DATE", "2025-01-01T00:00:00Z")
|
||||
.env("GIT_COMMITTER_DATE", "2025-01-01T00:00:00Z")
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
};
|
||||
git(&["init", "-q"]);
|
||||
git(&["add", "source.txt"]);
|
||||
git(&["commit", "-qm", "Original"]);
|
||||
fixture.file("source.txt", "first\nchanged\n");
|
||||
git(&["add", "source.txt"]);
|
||||
git(&["commit", "-qm", "Second"]);
|
||||
let mut session = EditorSession::open(Some(path.clone()), vec![], None).unwrap();
|
||||
let ready = |session: &mut EditorSession| {
|
||||
let deadline = Instant::now() + Duration::from_secs(10);
|
||||
while session.blame_lines.is_none() {
|
||||
assert!(Instant::now() < deadline, "file attribution did not finish");
|
||||
session.poll();
|
||||
thread::sleep(Duration::from_millis(5));
|
||||
}
|
||||
};
|
||||
let first = session.blame_request_id;
|
||||
session.handle(Input::Down);
|
||||
assert_eq!(session.blame_request_id, first);
|
||||
ready(&mut session);
|
||||
assert!(session.editor.line_blame().unwrap().contains("Second"));
|
||||
session.handle(Input::Up);
|
||||
assert!(session.editor.line_blame().unwrap().contains("Original"));
|
||||
session.handle(Input::Down);
|
||||
assert!(session.editor.line_blame().unwrap().contains("Second"));
|
||||
session.handle(Input::Down);
|
||||
assert!(session.editor.line_blame().is_none());
|
||||
assert_eq!(session.blame_request_id, first);
|
||||
session.handle(Input::Up);
|
||||
session.handle(Input::Char('i'));
|
||||
session.handle(Input::Char('x'));
|
||||
assert_ne!(session.blame_request_id, first);
|
||||
ready(&mut session);
|
||||
assert_eq!(session.editor.line_blame(), Some("Uncommitted"));
|
||||
session.handle(Input::Escape);
|
||||
let edited = session.blame_request_id;
|
||||
session.execute("save");
|
||||
assert_ne!(session.blame_request_id, edited);
|
||||
ready(&mut session);
|
||||
git(&["add", "source.txt"]);
|
||||
git(&["commit", "-qm", "Saved"]);
|
||||
let saved = session.blame_request_id;
|
||||
session.refresh_blame();
|
||||
assert_ne!(session.blame_request_id, saved);
|
||||
ready(&mut session);
|
||||
assert!(session.editor.line_blame().unwrap().contains("Saved"));
|
||||
fixture.file("source.txt", "first\nxchanged\n");
|
||||
let refreshed = session.blame_request_id;
|
||||
assert!(!session.editor.refresh_files());
|
||||
session.synchronize_documents();
|
||||
assert_ne!(session.blame_request_id, refreshed);
|
||||
ready(&mut session);
|
||||
session.handle(Input::Char('i'));
|
||||
session.handle(Input::Char('x'));
|
||||
ready(&mut session);
|
||||
assert!(session.editor.dirty());
|
||||
fs::remove_file(path).unwrap();
|
||||
assert!(session.editor.refresh_files());
|
||||
session.synchronize_documents();
|
||||
assert!(session.blame_source.is_none());
|
||||
assert!(session.editor.line_blame().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_navigation_resolves_utf16_and_validates_full_ranges_atomically() {
|
||||
use crate::plugin::Utf16Range;
|
||||
|
||||
Reference in New Issue
Block a user