feat: show repository changes in normal file pickers
This commit is contained in:
+181
-9
@@ -170,6 +170,7 @@ struct GutterSource {
|
||||
revision: u64,
|
||||
save_revision: u64,
|
||||
path: PathBuf,
|
||||
dirty: bool,
|
||||
contents: Arc<str>,
|
||||
}
|
||||
|
||||
@@ -198,6 +199,7 @@ pub struct EditorSession {
|
||||
gutter_request_id: u64,
|
||||
gutter_pending: bool,
|
||||
last_gutter_refresh: Option<Instant>,
|
||||
picker_source: Option<(usize, PathBuf)>,
|
||||
battery: Option<crate::battery::Battery>,
|
||||
battery_enabled: bool,
|
||||
last_file_refresh: Instant,
|
||||
@@ -312,6 +314,7 @@ impl EditorSession {
|
||||
gutter_request_id: 0,
|
||||
gutter_pending: false,
|
||||
last_gutter_refresh: None,
|
||||
picker_source: None,
|
||||
battery: None,
|
||||
battery_enabled: false,
|
||||
last_file_refresh: Instant::now(),
|
||||
@@ -356,6 +359,14 @@ impl EditorSession {
|
||||
}
|
||||
|
||||
pub fn handle(&mut self, input: Input) {
|
||||
if input == Input::Ctrl('r')
|
||||
&& self
|
||||
.editor
|
||||
.normal_file_picker()
|
||||
.is_some_and(|picker| picker.focused)
|
||||
{
|
||||
self.last_gutter_refresh = None;
|
||||
}
|
||||
self.cancel_pending_popup_request();
|
||||
let invocation = self.editor.handle(input);
|
||||
self.synchronize_documents();
|
||||
@@ -1056,21 +1067,36 @@ impl EditorSession {
|
||||
}
|
||||
|
||||
fn poll_gutter_at(&mut self, now: Instant) -> bool {
|
||||
if self.editor.should_quit()
|
||||
|| self.editor.gutter_style() == crate::gutter::GutterStyle::None
|
||||
let line_gutter = self.editor.gutter_style() != crate::gutter::GutterStyle::None;
|
||||
let picker_changed = match (self.editor.normal_file_picker(), &self.picker_source) {
|
||||
(Some(picker), Some((tab, root))) => {
|
||||
*tab != self.editor.tab_id() || *root != picker.root
|
||||
}
|
||||
(None, None) => false,
|
||||
_ => true,
|
||||
};
|
||||
if self.editor.should_quit() || (!line_gutter && self.editor.normal_file_picker().is_none())
|
||||
{
|
||||
self.gutter_worker = None;
|
||||
self.gutter_sources.clear();
|
||||
self.gutter_pending = false;
|
||||
self.picker_source = None;
|
||||
return false;
|
||||
}
|
||||
let sources_changed = !self
|
||||
.editor
|
||||
.documents()
|
||||
.filter(|document| line_gutter || document.dirty)
|
||||
.filter_map(|document| {
|
||||
document
|
||||
.path
|
||||
.map(|path| (document.id, document.revision, document.save_revision, path))
|
||||
document.path.map(|path| {
|
||||
(
|
||||
document.id,
|
||||
document.revision,
|
||||
document.save_revision,
|
||||
path,
|
||||
document.dirty,
|
||||
)
|
||||
})
|
||||
})
|
||||
.eq(self.gutter_sources.iter().map(|source| {
|
||||
(
|
||||
@@ -1078,17 +1104,25 @@ impl EditorSession {
|
||||
source.revision,
|
||||
source.save_revision,
|
||||
source.path.as_path(),
|
||||
source.dirty,
|
||||
)
|
||||
}));
|
||||
let refresh_due = !self.gutter_pending
|
||||
&& self
|
||||
.last_gutter_refresh
|
||||
.is_none_or(|last| now.saturating_duration_since(last) >= Duration::from_secs(1));
|
||||
if sources_changed || refresh_due {
|
||||
if sources_changed || picker_changed || refresh_due {
|
||||
if picker_changed {
|
||||
self.picker_source = self
|
||||
.editor
|
||||
.normal_file_picker()
|
||||
.map(|picker| (self.editor.tab_id(), picker.root.clone()));
|
||||
}
|
||||
if sources_changed {
|
||||
self.gutter_sources = self
|
||||
.editor
|
||||
.documents()
|
||||
.filter(|document| line_gutter || document.dirty)
|
||||
.filter_map(|document| {
|
||||
let path = document.path?;
|
||||
let contents = self
|
||||
@@ -1108,6 +1142,7 @@ impl EditorSession {
|
||||
revision: document.revision,
|
||||
save_revision: document.save_revision,
|
||||
path: path.to_path_buf(),
|
||||
dirty: document.dirty,
|
||||
contents,
|
||||
})
|
||||
})
|
||||
@@ -1115,7 +1150,7 @@ impl EditorSession {
|
||||
}
|
||||
self.gutter_request_id = self.gutter_request_id.wrapping_add(1);
|
||||
self.last_gutter_refresh = Some(now);
|
||||
if self.gutter_sources.is_empty() {
|
||||
if self.gutter_sources.is_empty() && self.picker_source.is_none() {
|
||||
self.gutter_worker = None;
|
||||
self.gutter_pending = false;
|
||||
return false;
|
||||
@@ -1132,6 +1167,7 @@ impl EditorSession {
|
||||
let files = self
|
||||
.gutter_sources
|
||||
.iter()
|
||||
.filter(|_| line_gutter)
|
||||
.map(|source| {
|
||||
(
|
||||
source.buffer,
|
||||
@@ -1140,11 +1176,20 @@ impl EditorSession {
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let picker = self.picker_source.as_ref().map(|(_, root)| {
|
||||
let buffers = self
|
||||
.gutter_sources
|
||||
.iter()
|
||||
.filter(|source| source.dirty)
|
||||
.map(|source| (source.path.clone(), Arc::clone(&source.contents)))
|
||||
.collect();
|
||||
(root.clone(), buffers)
|
||||
});
|
||||
match self
|
||||
.gutter_worker
|
||||
.as_mut()
|
||||
.expect("gutter worker started")
|
||||
.request(self.gutter_request_id, files)
|
||||
.request(self.gutter_request_id, files, picker)
|
||||
{
|
||||
Ok(()) => self.gutter_pending = true,
|
||||
Err(error) => {
|
||||
@@ -1156,12 +1201,15 @@ impl EditorSession {
|
||||
}
|
||||
}
|
||||
let mut changed = false;
|
||||
while let Some((id, result)) = self
|
||||
while let Some((id, result, picker)) = self
|
||||
.gutter_worker
|
||||
.as_mut()
|
||||
.and_then(crate::gutter::Worker::poll)
|
||||
{
|
||||
changed |= self.apply_gutter_result(id, result);
|
||||
if let Some(picker) = picker {
|
||||
changed |= self.apply_picker_result(id, picker);
|
||||
}
|
||||
}
|
||||
changed
|
||||
}
|
||||
@@ -1195,6 +1243,36 @@ impl EditorSession {
|
||||
changed
|
||||
}
|
||||
|
||||
fn apply_picker_result(&mut self, id: u64, reply: crate::gutter::PickerChanges) -> bool {
|
||||
let Some((tab, root)) = &self.picker_source else {
|
||||
return false;
|
||||
};
|
||||
if id != self.gutter_request_id
|
||||
|| *tab != self.editor.tab_id()
|
||||
|| *root != reply.root
|
||||
|| self
|
||||
.editor
|
||||
.normal_file_picker()
|
||||
.is_none_or(|picker| picker.root != *root)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let tab = *tab;
|
||||
let mut changed = false;
|
||||
let changes = match reply.result {
|
||||
Ok(changes) => changes,
|
||||
Err(error) => {
|
||||
self.editor.set_message(format!("File status: {error}"));
|
||||
changed = true;
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
changed
|
||||
| self
|
||||
.editor
|
||||
.set_picker_repository_changes(tab, &reply.root, changes)
|
||||
}
|
||||
|
||||
pub(crate) fn refresh_gutter(&mut self) -> bool {
|
||||
self.last_gutter_refresh = None;
|
||||
self.poll_gutter_at(Instant::now())
|
||||
@@ -1399,6 +1477,100 @@ mod tests {
|
||||
sync::atomic::{AtomicU64, Ordering},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn normal_picker_tracks_unsaved_text_and_head_with_line_gutter_disabled() {
|
||||
use crate::diff::FileStatus;
|
||||
let fixture = Fixture::new();
|
||||
fixture.file("buffer.txt", "base\n");
|
||||
fixture.file("clean.txt", "clean\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", "--template="]);
|
||||
git(&["add", "buffer.txt", "clean.txt"]);
|
||||
git(&["commit", "-qm", "baseline"]);
|
||||
fixture.file("buffer.txt", "disk\n");
|
||||
fixture.file("added.txt", "new\n");
|
||||
let mut session = fixture.open(
|
||||
"Vamp.configure do |config|\nconfig.git_gutter = \"none\"\nconfig.git_blame = false\nend\n",
|
||||
);
|
||||
session.editor.set_project_root(fixture.0.clone());
|
||||
let now = session.last_file_refresh;
|
||||
let open = |session: &mut EditorSession| {
|
||||
session.handle(Input::Char(' '));
|
||||
session.handle(Input::Char('o'));
|
||||
};
|
||||
let ready = |session: &mut EditorSession, clock: Instant, expected| {
|
||||
let deadline = Instant::now() + Duration::from_secs(10);
|
||||
loop {
|
||||
session.poll_gutter_at(clock);
|
||||
let picker = session.editor.normal_file_picker().unwrap();
|
||||
let status = picker
|
||||
.entries()
|
||||
.find(|row| row.path == Path::new("buffer.txt"))
|
||||
.expect("normal picker retained the buffer file")
|
||||
.status;
|
||||
if !session.gutter_pending && status == expected {
|
||||
assert!(
|
||||
picker.entries().any(|row| {
|
||||
row.path == Path::new("clean.txt") && row.status.is_none()
|
||||
})
|
||||
);
|
||||
assert!(picker.entries().any(|row| {
|
||||
row.path == Path::new("added.txt") && row.status == Some(FileStatus::Added)
|
||||
}));
|
||||
break;
|
||||
}
|
||||
assert!(
|
||||
Instant::now() < deadline,
|
||||
"normal picker status did not settle: {}",
|
||||
session.editor.message()
|
||||
);
|
||||
thread::sleep(Duration::from_millis(5));
|
||||
}
|
||||
};
|
||||
open(&mut session);
|
||||
ready(&mut session, now, Some(FileStatus::Modified));
|
||||
session.handle(Input::Ctrl('w'));
|
||||
session.poll_gutter_at(now);
|
||||
session
|
||||
.editor
|
||||
.apply(vec![Effect::Replace("base\n".into())])
|
||||
.unwrap();
|
||||
assert!(session.editor.dirty());
|
||||
open(&mut session);
|
||||
ready(&mut session, now, None);
|
||||
assert_eq!(
|
||||
fs::read_to_string(fixture.0.join("buffer.txt")).unwrap(),
|
||||
"disk\n"
|
||||
);
|
||||
session.handle(Input::Ctrl('w'));
|
||||
session.poll_gutter_at(now);
|
||||
session.handle(Input::Char('u'));
|
||||
open(&mut session);
|
||||
ready(&mut session, now, Some(FileStatus::Modified));
|
||||
git(&["add", "buffer.txt"]);
|
||||
git(&["commit", "-qm", "new baseline"]);
|
||||
ready(&mut session, now + Duration::from_secs(1), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gutter_results_follow_buffer_identity_and_reject_stale_edits() {
|
||||
use crate::gutter::Change;
|
||||
|
||||
Reference in New Issue
Block a user