feat: show repository changes in normal file pickers

This commit is contained in:
2026-10-13 21:10:20 +02:00
parent e90d1c3945
commit 9526876b04
12 changed files with 2169 additions and 33 deletions
+99 -11
View File
@@ -3,7 +3,9 @@ use crate::{
blame::{git, git_without_literal_pathspecs, run, run_status},
diff::line_hunks,
editor::normalize_newlines,
repository::{jj_command, jj_file_pattern, safe_path, store_for_jj},
repository::{
WorkingFileChange, WorkingFileReader, jj_command, jj_file_pattern, safe_path, store_for_jj,
},
};
use serde::Deserialize;
use std::{
@@ -23,7 +25,13 @@ const FILE_COUNT_LIMIT: usize = 1024;
type Files = Vec<(usize, PathBuf, Arc<str>)>;
type FileResult = Result<Vec<Option<Change>>, String>;
pub(crate) type ResultBatch = Vec<(usize, FileResult)>;
type Request = (u64, u64, Files);
type PickerRequest = (PathBuf, Vec<(PathBuf, Arc<str>)>);
type Request = (u64, u64, Files, Option<PickerRequest>);
pub(crate) struct PickerChanges {
pub(crate) root: PathBuf,
pub(crate) result: Result<Vec<WorkingFileChange>, String>,
}
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Deserialize)]
#[serde(rename_all = "snake_case")]
@@ -45,7 +53,7 @@ pub(crate) enum Change {
#[derive(Default)]
struct State {
request: Option<Request>,
result: Option<(u64, ResultBatch)>,
result: Option<(u64, ResultBatch, Option<PickerChanges>)>,
}
#[derive(Default)]
struct Shared {
@@ -65,6 +73,7 @@ impl Worker {
let thread = thread::Builder::new()
.name("vamp-gutter".into())
.spawn(move || {
let mut picker_reader = WorkingFileReader::default();
loop {
let Ok(mut state) = worker.state.lock() else {
return;
@@ -78,7 +87,7 @@ impl Worker {
if worker.stopped.load(Ordering::Acquire) {
return;
}
let (generation, id, files) = state.request.take().unwrap();
let (generation, id, files, picker) = state.request.take().unwrap();
drop(state);
let cancelled = || {
worker.stopped.load(Ordering::Acquire)
@@ -91,11 +100,15 @@ impl Worker {
}
results.push((buffer, read_file(&path, &contents, &cancelled)));
}
let picker = picker.map(|(root, buffers)| PickerChanges {
result: picker_reader.read(&root, &buffers, &cancelled),
root,
});
let Ok(mut state) = worker.state.lock() else {
return;
};
if !cancelled() {
state.result = Some((id, results));
state.result = Some((id, results, picker));
}
}
})?;
@@ -105,12 +118,31 @@ impl Worker {
})
}
pub(crate) fn request(&mut self, id: u64, files: Files) -> io::Result<()> {
pub(crate) fn request(
&mut self,
id: u64,
files: Files,
picker: Option<PickerRequest>,
) -> io::Result<()> {
let size = files.iter().try_fold(0usize, |size, (_, path, text)| {
size.checked_add(text.len())?
.checked_add(path.as_os_str().len())
});
if files.len() > FILE_COUNT_LIMIT || size.is_none_or(|size| size > BATCH_LIMIT) {
let picker_size = picker.as_ref().map_or(Some(0), |(root, buffers)| {
buffers
.iter()
.try_fold(root.as_os_str().len(), |size, (path, text)| {
size.checked_add(text.len())?
.checked_add(path.as_os_str().len())
})
});
let count = files.len() + picker.as_ref().map_or(0, |(_, buffers)| buffers.len());
if count > FILE_COUNT_LIMIT
|| size
.zip(picker_size)
.and_then(|(files, picker)| files.checked_add(picker))
.is_none_or(|size| size > BATCH_LIMIT)
{
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Gutter batch exceeds 64 MiB or 1024 files",
@@ -126,13 +158,13 @@ impl Worker {
.generation
.fetch_add(1, Ordering::AcqRel)
.wrapping_add(1);
state.request = Some((generation, id, files));
state.request = Some((generation, id, files, picker));
state.result = None;
self.shared.wake.notify_one();
Ok(())
}
pub(crate) fn poll(&mut self) -> Option<(u64, ResultBatch)> {
pub(crate) fn poll(&mut self) -> Option<(u64, ResultBatch, Option<PickerChanges>)> {
self.shared.state.lock().ok()?.result.take()
}
}
@@ -766,11 +798,13 @@ mod tests {
(5, path.clone(), "first\nchanged\nthird\n".into()),
(8, path.clone(), Arc::clone(&oversized)),
],
None,
)
.unwrap();
}
loop {
if let Some((id, result)) = worker.poll() {
if let Some((id, result, picker)) = worker.poll() {
assert!(picker.is_none());
assert_eq!(id, 19);
assert_eq!(result[0].0, 5);
assert_eq!(
@@ -788,10 +822,64 @@ mod tests {
}
assert_eq!(
worker
.request(20, vec![(0, path, "x".repeat(BATCH_LIMIT + 1).into())])
.request(
20,
vec![(0, path, "x".repeat(BATCH_LIMIT + 1).into())],
None
)
.unwrap_err()
.kind(),
io::ErrorKind::InvalidInput
);
}
#[test]
fn picker_status_is_optional_and_errors_do_not_discard_line_results() {
let (temp, path) = git_fixture();
let mut worker = Worker::start().unwrap();
worker
.request(
1,
vec![],
Some((
temp.0.clone(),
vec![(path.clone(), "dirty buffer\n".into())],
)),
)
.unwrap();
loop {
if let Some((id, lines, picker)) = worker.poll() {
assert_eq!(id, 1);
assert!(lines.is_empty());
let picker = picker.unwrap();
assert_eq!(picker.root, temp.0);
let changes = picker.result.unwrap();
assert_eq!(changes.len(), 1);
assert_eq!(changes[0].path, path.strip_prefix(&temp.0).unwrap());
assert_eq!(changes[0].status, crate::diff::FileStatus::Modified);
break;
}
thread::yield_now();
}
worker
.request(
2,
vec![(4, path.clone(), "first\nchanged\nthird\n".into())],
Some((
temp.0.clone(),
vec![(path, "x".repeat(FILE_LIMIT + 1).into())],
)),
)
.unwrap();
loop {
if let Some((id, lines, picker)) = worker.poll() {
assert_eq!(id, 2);
assert_eq!(lines[0].0, 4);
assert!(lines[0].1.is_ok());
assert!(picker.unwrap().result.unwrap_err().contains("16 MiB"));
break;
}
thread::yield_now();
}
}
}