project_panel: Add file comparison function, supports selecting files for comparison (#35255)

Closes https://github.com/zed-industries/zed/discussions/35010
Closes https://github.com/zed-industries/zed/issues/17100
Closes https://github.com/zed-industries/zed/issues/4523

Release Notes:

- Added file comparison function in project panel

---------

Co-authored-by: Kirill Bulatov <kirill@zed.dev>
This commit is contained in:
mcwindy 2025-08-08 02:34:12 +08:00 committed by GitHub
parent 53b69d29c5
commit e8db429d24
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 295 additions and 45 deletions

View file

@ -8,7 +8,7 @@ use settings::SettingsStore;
use std::path::{Path, PathBuf};
use util::path;
use workspace::{
AppState, Pane,
AppState, ItemHandle, Pane,
item::{Item, ProjectItem},
register_project_item,
};
@ -3068,7 +3068,7 @@ async fn test_multiple_marked_entries(cx: &mut gpui::TestAppContext) {
panel.update(cx, |this, cx| {
let drag = DraggedSelection {
active_selection: this.selection.unwrap(),
marked_selections: Arc::new(this.marked_entries.clone()),
marked_selections: this.marked_entries.clone().into(),
};
let target_entry = this
.project
@ -5562,10 +5562,10 @@ async fn test_highlight_entry_for_selection_drag(cx: &mut gpui::TestAppContext)
worktree_id,
entry_id: child_file.id,
},
marked_selections: Arc::new(BTreeSet::from([SelectedEntry {
marked_selections: Arc::new([SelectedEntry {
worktree_id,
entry_id: child_file.id,
}])),
}]),
};
let result =
panel.highlight_entry_for_selection_drag(parent_dir, worktree, &dragged_selection, cx);
@ -5604,7 +5604,7 @@ async fn test_highlight_entry_for_selection_drag(cx: &mut gpui::TestAppContext)
worktree_id,
entry_id: child_file.id,
},
marked_selections: Arc::new(BTreeSet::from([
marked_selections: Arc::new([
SelectedEntry {
worktree_id,
entry_id: child_file.id,
@ -5613,7 +5613,7 @@ async fn test_highlight_entry_for_selection_drag(cx: &mut gpui::TestAppContext)
worktree_id,
entry_id: sibling_file.id,
},
])),
]),
};
let result =
panel.highlight_entry_for_selection_drag(parent_dir, worktree, &dragged_selection, cx);
@ -5821,6 +5821,186 @@ async fn test_hide_root(cx: &mut gpui::TestAppContext) {
}
}
#[gpui::test]
async fn test_compare_selected_files(cx: &mut gpui::TestAppContext) {
init_test_with_editor(cx);
let fs = FakeFs::new(cx.executor().clone());
fs.insert_tree(
"/root",
json!({
"file1.txt": "content of file1",
"file2.txt": "content of file2",
"dir1": {
"file3.txt": "content of file3"
}
}),
)
.await;
let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let panel = workspace.update(cx, ProjectPanel::new).unwrap();
let file1_path = path!("root/file1.txt");
let file2_path = path!("root/file2.txt");
select_path_with_mark(&panel, file1_path, cx);
select_path_with_mark(&panel, file2_path, cx);
panel.update_in(cx, |panel, window, cx| {
panel.compare_marked_files(&CompareMarkedFiles, window, cx);
});
cx.executor().run_until_parked();
workspace
.update(cx, |workspace, _, cx| {
let active_items = workspace
.panes()
.iter()
.filter_map(|pane| pane.read(cx).active_item())
.collect::<Vec<_>>();
assert_eq!(active_items.len(), 1);
let diff_view = active_items
.into_iter()
.next()
.unwrap()
.downcast::<FileDiffView>()
.expect("Open item should be an FileDiffView");
assert_eq!(diff_view.tab_content_text(0, cx), "file1.txt ↔ file2.txt");
assert_eq!(
diff_view.tab_tooltip_text(cx).unwrap(),
format!("{}{}", file1_path, file2_path)
);
})
.unwrap();
let file1_entry_id = find_project_entry(&panel, file1_path, cx).unwrap();
let file2_entry_id = find_project_entry(&panel, file2_path, cx).unwrap();
let worktree_id = panel.update(cx, |panel, cx| {
panel
.project
.read(cx)
.worktrees(cx)
.next()
.unwrap()
.read(cx)
.id()
});
let expected_entries = [
SelectedEntry {
worktree_id,
entry_id: file1_entry_id,
},
SelectedEntry {
worktree_id,
entry_id: file2_entry_id,
},
];
panel.update(cx, |panel, _cx| {
assert_eq!(
&panel.marked_entries, &expected_entries,
"Should keep marked entries after comparison"
);
});
panel.update(cx, |panel, cx| {
panel.project.update(cx, |_, cx| {
cx.emit(project::Event::RevealInProjectPanel(file2_entry_id))
})
});
panel.update(cx, |panel, _cx| {
assert_eq!(
&panel.marked_entries, &expected_entries,
"Marked entries should persist after focusing back on the project panel"
);
});
}
#[gpui::test]
async fn test_compare_files_context_menu(cx: &mut gpui::TestAppContext) {
init_test_with_editor(cx);
let fs = FakeFs::new(cx.executor().clone());
fs.insert_tree(
"/root",
json!({
"file1.txt": "content of file1",
"file2.txt": "content of file2",
"dir1": {},
"dir2": {
"file3.txt": "content of file3"
}
}),
)
.await;
let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await;
let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*workspace, cx);
let panel = workspace.update(cx, ProjectPanel::new).unwrap();
// Test 1: When only one file is selected, there should be no compare option
select_path(&panel, "root/file1.txt", cx);
let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
assert_eq!(
selected_files, None,
"Should not have compare option when only one file is selected"
);
// Test 2: When multiple files are selected, there should be a compare option
select_path_with_mark(&panel, "root/file1.txt", cx);
select_path_with_mark(&panel, "root/file2.txt", cx);
let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
assert!(
selected_files.is_some(),
"Should have files selected for comparison"
);
if let Some((file1, file2)) = selected_files {
assert!(
file1.to_string_lossy().ends_with("file1.txt")
&& file2.to_string_lossy().ends_with("file2.txt"),
"Should have file1.txt and file2.txt as the selected files when multi-selecting"
);
}
// Test 3: Selecting a directory shouldn't count as a comparable file
select_path_with_mark(&panel, "root/dir1", cx);
let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
assert!(
selected_files.is_some(),
"Directory selection should not affect comparable files"
);
if let Some((file1, file2)) = selected_files {
assert!(
file1.to_string_lossy().ends_with("file1.txt")
&& file2.to_string_lossy().ends_with("file2.txt"),
"Selecting a directory should not affect the number of comparable files"
);
}
// Test 4: Selecting one more file
select_path_with_mark(&panel, "root/dir2/file3.txt", cx);
let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx));
assert!(
selected_files.is_some(),
"Directory selection should not affect comparable files"
);
if let Some((file1, file2)) = selected_files {
assert!(
file1.to_string_lossy().ends_with("file2.txt")
&& file2.to_string_lossy().ends_with("file3.txt"),
"Selecting a directory should not affect the number of comparable files"
);
}
}
fn select_path(panel: &Entity<ProjectPanel>, path: impl AsRef<Path>, cx: &mut VisualTestContext) {
let path = path.as_ref();
panel.update(cx, |panel, cx| {
@ -5855,7 +6035,7 @@ fn select_path_with_mark(
entry_id,
};
if !panel.marked_entries.contains(&entry) {
panel.marked_entries.insert(entry);
panel.marked_entries.push(entry);
}
panel.selection = Some(entry);
return;