fix: retain repository status between picker openings
This commit is contained in:
+19
-12
@@ -1830,6 +1830,7 @@ pub struct Editor {
|
||||
startup_diagnostic: Option<StartupDiagnostic>,
|
||||
project_root: PathBuf,
|
||||
file_picker: Option<FilePicker>,
|
||||
picker_repository_changes: Vec<crate::repository::WorkingFileChange>,
|
||||
file_explorer_side: DockSide,
|
||||
file_picker_view: FileView,
|
||||
file_explorer_view: FileView,
|
||||
@@ -1958,6 +1959,7 @@ impl Editor {
|
||||
startup_diagnostic: None,
|
||||
project_root: std::env::current_dir()?,
|
||||
file_picker: None,
|
||||
picker_repository_changes: Vec::new(),
|
||||
file_explorer_side: DockSide::Right,
|
||||
file_picker_view: FileView::Tree,
|
||||
file_explorer_view: FileView::Tree,
|
||||
@@ -4790,21 +4792,24 @@ impl Editor {
|
||||
|
||||
pub(crate) fn set_picker_repository_changes(
|
||||
&mut self,
|
||||
tab: usize,
|
||||
root: &Path,
|
||||
changes: Vec<crate::repository::WorkingFileChange>,
|
||||
) -> bool {
|
||||
let picker = if tab == self.tab_id {
|
||||
self.file_picker.as_mut().filter(|_| self.diff.is_none())
|
||||
} else {
|
||||
self.tabs
|
||||
.get_mut(&tab)
|
||||
.and_then(|tab| tab.file_picker.as_mut().filter(|_| tab.diff.is_none()))
|
||||
};
|
||||
let Some(picker) = picker.filter(|picker| picker.root == root) else {
|
||||
if root != self.project_root {
|
||||
return false;
|
||||
};
|
||||
picker.set_repository_changes(changes)
|
||||
}
|
||||
let mut changed = false;
|
||||
for picker in self.file_picker.iter_mut().chain(
|
||||
self.tabs
|
||||
.values_mut()
|
||||
.filter_map(|tab| tab.file_picker.as_mut()),
|
||||
) {
|
||||
if picker.root == root {
|
||||
changed |= picker.set_repository_changes(changes.clone());
|
||||
}
|
||||
}
|
||||
self.picker_repository_changes = changes;
|
||||
changed
|
||||
}
|
||||
|
||||
pub fn file_explorer_side(&self) -> DockSide {
|
||||
@@ -4861,6 +4866,7 @@ impl Editor {
|
||||
#[cfg(test)]
|
||||
pub(crate) fn set_project_root(&mut self, root: PathBuf) {
|
||||
self.project_root = root;
|
||||
self.picker_repository_changes.clear();
|
||||
}
|
||||
/// Pending decimal prefix for the next Normal/Visual motion or editing action.
|
||||
pub fn count(&self) -> Option<usize> {
|
||||
@@ -5950,7 +5956,8 @@ impl Editor {
|
||||
picker.clear_error();
|
||||
} else {
|
||||
match FilePicker::scan(self.project_root.clone(), presentation, view) {
|
||||
Ok(picker) => {
|
||||
Ok(mut picker) => {
|
||||
picker.set_repository_changes(self.picker_repository_changes.clone());
|
||||
self.file_picker = Some(picker);
|
||||
}
|
||||
Err(error) => {
|
||||
|
||||
@@ -101,7 +101,6 @@ pub struct FilePicker {
|
||||
tree_searching: bool,
|
||||
warning: Option<String>,
|
||||
repository_changes: Vec<WorkingFileChange>,
|
||||
has_repository_changes: bool,
|
||||
has_virtual_entries: bool,
|
||||
}
|
||||
|
||||
@@ -307,7 +306,6 @@ impl FilePicker {
|
||||
tree_searching: false,
|
||||
warning,
|
||||
repository_changes: Vec::new(),
|
||||
has_repository_changes: false,
|
||||
};
|
||||
for root in roots {
|
||||
let tree = picker.compact(root, None);
|
||||
@@ -780,10 +778,6 @@ impl FilePicker {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn has_repository_changes(&self) -> bool {
|
||||
self.has_repository_changes
|
||||
}
|
||||
|
||||
pub(crate) fn set_repository_changes(&mut self, changes: Vec<WorkingFileChange>) -> bool {
|
||||
if !self.has_virtual_entries && changes == self.repository_changes {
|
||||
return false;
|
||||
@@ -919,7 +913,6 @@ impl FilePicker {
|
||||
replacement.error = self.error.clone();
|
||||
self.replace_catalog(replacement);
|
||||
}
|
||||
self.has_repository_changes = self.nodes.iter().any(|node| node.status.is_some());
|
||||
self.repository_changes = changes;
|
||||
topology_changed || metadata_changed
|
||||
}
|
||||
@@ -1125,7 +1118,6 @@ mod tests {
|
||||
assert_eq!(picker.query, "src");
|
||||
assert!(!picker.focused);
|
||||
assert_eq!(picker.presentation, FilePresentation::Docked);
|
||||
assert!(picker.has_repository_changes());
|
||||
for view in [FileView::List, FileView::Tree] {
|
||||
picker.set_view(view);
|
||||
for query in ["", "src", "modified.rs", "deleted.rs"] {
|
||||
@@ -1166,7 +1158,6 @@ mod tests {
|
||||
picker.reveal_path(Path::new("src/clean.rs"));
|
||||
assert!(picker.set_repository_changes(Vec::new()));
|
||||
assert_eq!(selected_path(&picker), Path::new("src/clean.rs"));
|
||||
assert!(!picker.has_repository_changes());
|
||||
assert_eq!(picker.entries().len(), 4);
|
||||
assert!(picker.nodes.iter().all(|node| !node.virtual_entry));
|
||||
assert!(
|
||||
|
||||
+87
-28
@@ -376,7 +376,7 @@ pub struct EditorSession {
|
||||
gutter_request_id: u64,
|
||||
gutter_pending: bool,
|
||||
last_gutter_refresh: Option<Instant>,
|
||||
picker_source: Option<(usize, PathBuf)>,
|
||||
picker_source: Option<PathBuf>,
|
||||
battery: Option<crate::battery::Battery>,
|
||||
battery_enabled: bool,
|
||||
last_file_refresh: Instant,
|
||||
@@ -1275,15 +1275,8 @@ impl EditorSession {
|
||||
|
||||
fn poll_gutter_at(&mut self, now: Instant) -> bool {
|
||||
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())
|
||||
{
|
||||
let picker_changed = self.picker_source.as_deref() != Some(self.editor.project_root());
|
||||
if self.editor.should_quit() {
|
||||
self.gutter_worker = None;
|
||||
self.gutter_sources.clear();
|
||||
self.gutter_pending = false;
|
||||
@@ -1320,10 +1313,7 @@ impl EditorSession {
|
||||
.is_none_or(|last| now.saturating_duration_since(last) >= Duration::from_secs(1));
|
||||
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()));
|
||||
self.picker_source = Some(self.editor.project_root().to_owned());
|
||||
}
|
||||
if sources_changed {
|
||||
self.gutter_sources = self
|
||||
@@ -1357,11 +1347,6 @@ 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() && self.picker_source.is_none() {
|
||||
self.gutter_worker = None;
|
||||
self.gutter_pending = false;
|
||||
return false;
|
||||
}
|
||||
if self.gutter_worker.is_none() {
|
||||
match crate::gutter::Worker::start() {
|
||||
Ok(worker) => self.gutter_worker = Some(worker),
|
||||
@@ -1383,7 +1368,7 @@ impl EditorSession {
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let picker = self.picker_source.as_ref().map(|(_, root)| {
|
||||
let picker = self.picker_source.as_ref().map(|root| {
|
||||
let buffers = self
|
||||
.gutter_sources
|
||||
.iter()
|
||||
@@ -1451,20 +1436,15 @@ impl EditorSession {
|
||||
}
|
||||
|
||||
fn apply_picker_result(&mut self, id: u64, reply: crate::gutter::PickerChanges) -> bool {
|
||||
let Some((tab, root)) = &self.picker_source else {
|
||||
let Some(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)
|
||||
|| *root != self.editor.project_root()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let tab = *tab;
|
||||
let mut changed = false;
|
||||
let changes = match reply.result {
|
||||
Ok(changes) => changes,
|
||||
@@ -1477,7 +1457,7 @@ impl EditorSession {
|
||||
changed
|
||||
| self
|
||||
.editor
|
||||
.set_picker_repository_changes(tab, &reply.root, changes)
|
||||
.set_picker_repository_changes(&reply.root, changes)
|
||||
}
|
||||
|
||||
pub(crate) fn refresh_gutter(&mut self) -> bool {
|
||||
@@ -1816,6 +1796,16 @@ mod tests {
|
||||
session.handle(Input::Char(' '));
|
||||
session.handle(Input::Char('o'));
|
||||
};
|
||||
let status = |session: &EditorSession| {
|
||||
session
|
||||
.editor
|
||||
.normal_file_picker()
|
||||
.unwrap()
|
||||
.entries()
|
||||
.find(|row| row.path == Path::new("buffer.txt"))
|
||||
.unwrap()
|
||||
.status
|
||||
};
|
||||
let ready = |session: &mut EditorSession, clock: Instant, expected| {
|
||||
let deadline = Instant::now() + Duration::from_secs(10);
|
||||
loop {
|
||||
@@ -1845,16 +1835,56 @@ mod tests {
|
||||
thread::sleep(Duration::from_millis(5));
|
||||
}
|
||||
};
|
||||
let settle = |session: &mut EditorSession| {
|
||||
let deadline = Instant::now() + Duration::from_secs(10);
|
||||
loop {
|
||||
session.poll_gutter_at(now);
|
||||
if !session.gutter_pending {
|
||||
break;
|
||||
}
|
||||
assert!(
|
||||
Instant::now() < deadline,
|
||||
"background file status did not settle"
|
||||
);
|
||||
thread::sleep(Duration::from_millis(5));
|
||||
}
|
||||
};
|
||||
settle(&mut session);
|
||||
open(&mut session);
|
||||
assert_eq!(
|
||||
status(&session),
|
||||
Some(FileStatus::Modified),
|
||||
"first open must reuse status loaded while the picker was closed",
|
||||
);
|
||||
ready(&mut session, now, Some(FileStatus::Modified));
|
||||
session.handle(Input::Ctrl('w'));
|
||||
settle(&mut session);
|
||||
open(&mut session);
|
||||
assert_eq!(
|
||||
status(&session),
|
||||
Some(FileStatus::Modified),
|
||||
"reopening must retain completed status",
|
||||
);
|
||||
let request = session.gutter_request_id;
|
||||
session.poll_gutter_at(now);
|
||||
assert_eq!(
|
||||
session.gutter_request_id, request,
|
||||
"opening must not restart status work"
|
||||
);
|
||||
session.handle(Input::Ctrl('w'));
|
||||
session.poll_gutter_at(now);
|
||||
session
|
||||
.editor
|
||||
.apply(vec![Effect::Replace("base\n".into())])
|
||||
.unwrap();
|
||||
assert!(session.editor.dirty());
|
||||
settle(&mut session);
|
||||
open(&mut session);
|
||||
assert_eq!(
|
||||
status(&session),
|
||||
None,
|
||||
"closed-picker edits must refresh the cache"
|
||||
);
|
||||
ready(&mut session, now, None);
|
||||
assert_eq!(
|
||||
fs::read_to_string(fixture.0.join("buffer.txt")).unwrap(),
|
||||
@@ -1863,11 +1893,40 @@ mod tests {
|
||||
session.handle(Input::Ctrl('w'));
|
||||
session.poll_gutter_at(now);
|
||||
session.handle(Input::Char('u'));
|
||||
settle(&mut session);
|
||||
open(&mut session);
|
||||
assert_eq!(status(&session), Some(FileStatus::Modified));
|
||||
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);
|
||||
assert!(!session.apply_picker_result(
|
||||
session.gutter_request_id.wrapping_sub(1),
|
||||
crate::gutter::PickerChanges {
|
||||
root: fixture.0.clone(),
|
||||
result: Ok(Vec::new()),
|
||||
},
|
||||
));
|
||||
assert!(!session.apply_picker_result(
|
||||
session.gutter_request_id,
|
||||
crate::gutter::PickerChanges {
|
||||
root: fixture.0.join("elsewhere"),
|
||||
result: Ok(Vec::new()),
|
||||
},
|
||||
));
|
||||
session.handle(Input::Ctrl('w'));
|
||||
session.execute("tab.new");
|
||||
open(&mut session);
|
||||
assert_eq!(status(&session), None);
|
||||
assert!(
|
||||
session
|
||||
.editor
|
||||
.normal_file_picker()
|
||||
.unwrap()
|
||||
.entries()
|
||||
.any(|row| row.path == Path::new("added.txt")
|
||||
&& row.status == Some(FileStatus::Added))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+57
-10
@@ -4649,7 +4649,6 @@ fn draw_file_list(
|
||||
);
|
||||
}
|
||||
let entries = picker.entries();
|
||||
let normal_markers = changed_files.is_none() && picker.has_repository_changes();
|
||||
if entries.len() == 0 {
|
||||
frame.render_widget(
|
||||
Paragraph::new(if picker.query.is_empty() {
|
||||
@@ -4722,7 +4721,7 @@ fn draw_file_list(
|
||||
Style::default().fg(color),
|
||||
));
|
||||
spans.push(Span::raw(" "));
|
||||
} else if normal_markers {
|
||||
} else if changed_files.is_none() {
|
||||
spans.push(Span::raw(" "));
|
||||
}
|
||||
if status == Some(FileStatus::Renamed)
|
||||
@@ -7559,7 +7558,6 @@ mod tests {
|
||||
}
|
||||
}
|
||||
picker.refresh();
|
||||
assert!(picker.has_repository_changes());
|
||||
assert!(
|
||||
picker
|
||||
.entries()
|
||||
@@ -7581,13 +7579,57 @@ mod tests {
|
||||
})
|
||||
.unwrap();
|
||||
let text = screen_text(&terminal);
|
||||
assert!(text.contains("> clean.txt"));
|
||||
assert!(text.contains("r.txt"));
|
||||
assert!(!text.contains("old�name.txt"));
|
||||
assert!(!text.contains("d.txt"));
|
||||
assert!(!text.contains('✓'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normal_file_names_stay_put_when_status_arrives_or_clears() {
|
||||
let directory = Directory::new();
|
||||
directory.file("file.txt", "");
|
||||
let mut picker = FilePicker::scan(
|
||||
directory.0.clone(),
|
||||
FilePresentation::Floating,
|
||||
FileView::List,
|
||||
)
|
||||
.unwrap();
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(80, 12)).unwrap();
|
||||
for view in [FileView::List, FileView::Tree] {
|
||||
picker.set_view(view);
|
||||
let mut columns = Vec::new();
|
||||
for status in [None, Some(FileStatus::Modified), None] {
|
||||
picker.set_repository_changes(
|
||||
status
|
||||
.into_iter()
|
||||
.map(|status| crate::repository::WorkingFileChange {
|
||||
path: "file.txt".into(),
|
||||
status,
|
||||
old_path: None,
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
draw_file_list(
|
||||
frame,
|
||||
&picker,
|
||||
frame.area(),
|
||||
"",
|
||||
&mut WheelPanels::default(),
|
||||
None,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
let text = screen_text(&terminal);
|
||||
columns.push(text.lines().find_map(|row| row.find("file.txt")).unwrap());
|
||||
}
|
||||
assert_eq!(columns[0], columns[1], "status arrival moved the filename");
|
||||
assert_eq!(columns[1], columns[2], "clearing status moved the filename");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normal_status_prefix_clips_without_review_columns_or_painting_outside_tiny_bounds() {
|
||||
let directory = Directory::new();
|
||||
@@ -13167,7 +13209,7 @@ end
|
||||
.draw(|frame| draw_file_list(frame, &picker, area, "", &mut panels, None))
|
||||
.unwrap();
|
||||
assert!(panels.manual_files.is_none());
|
||||
assert!(screen_text(&terminal).contains("> file-10"));
|
||||
assert!(screen_text(&terminal).contains("file-10"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -13306,7 +13348,11 @@ end
|
||||
})
|
||||
.unwrap();
|
||||
let text = screen_text(&terminal);
|
||||
assert!(text.contains(&format!("> {label}")), "{text}");
|
||||
assert!(
|
||||
text.lines()
|
||||
.any(|row| row.contains(label) && row.contains('>')),
|
||||
"{text}"
|
||||
);
|
||||
let buffer = terminal.backend().buffer();
|
||||
let selected_y = (3..8).find(|&y| buffer[(1, y)].symbol() == ">").unwrap();
|
||||
assert_eq!(buffer[(1, selected_y)].bg, Color::Blue);
|
||||
@@ -13451,8 +13497,10 @@ end
|
||||
let selected_y = (area.y + 2..area.bottom() - 1)
|
||||
.find(|&y| buffer[(area.x + 1, y)].symbol() == ">")
|
||||
.unwrap();
|
||||
assert_eq!(buffer[(area.x + 3, selected_y)].symbol(), "界");
|
||||
assert_eq!(buffer[(area.x + 5, selected_y)].symbol(), "e\u{301}");
|
||||
let selected_x = (area.x + 2..area.right())
|
||||
.find(|&x| buffer[(x, selected_y)].symbol() == "界")
|
||||
.unwrap();
|
||||
assert_eq!(buffer[(selected_x + 2, selected_y)].symbol(), "e\u{301}");
|
||||
assert_eq!(buffer[(area.x + 1, selected_y)].bg, Color::Blue);
|
||||
assert!(
|
||||
buffer[(area.x + 1, selected_y)]
|
||||
@@ -13729,7 +13777,6 @@ end
|
||||
" nested/",
|
||||
" inner.txt",
|
||||
" sibling.txt",
|
||||
"> single/",
|
||||
" only.txt",
|
||||
" root.txt",
|
||||
] {
|
||||
@@ -13833,7 +13880,7 @@ end
|
||||
}
|
||||
if width == 100 {
|
||||
let text = screen_text(&terminal);
|
||||
assert!(text.contains("> file.txt"), "{text}");
|
||||
assert!(text.contains("file.txt"), "{text}");
|
||||
assert!(!text.contains("dir-00"), "{text}");
|
||||
}
|
||||
for query in ["missing", long_query.as_str()] {
|
||||
|
||||
Reference in New Issue
Block a user