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

mcwindy and Kirill Bulatov created

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>

Change summary

Cargo.lock                                      |   1 
assets/keymaps/default-linux.json               |   1 
assets/keymaps/default-macos.json               |   1 
assets/keymaps/vim.json                         |   1 
crates/project_panel/Cargo.toml                 |   1 
crates/project_panel/src/project_panel.rs       | 139 +++++++++---
crates/project_panel/src/project_panel_tests.rs | 194 ++++++++++++++++++
crates/workspace/src/pane.rs                    |   2 
8 files changed, 295 insertions(+), 45 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -12636,6 +12636,7 @@ dependencies = [
  "editor",
  "file_icons",
  "git",
+ "git_ui",
  "gpui",
  "indexmap",
  "language",

assets/keymaps/default-linux.json 🔗

@@ -848,6 +848,7 @@
       "ctrl-delete": ["project_panel::Delete", { "skip_prompt": false }],
       "alt-ctrl-r": "project_panel::RevealInFileManager",
       "ctrl-shift-enter": "project_panel::OpenWithSystem",
+      "alt-d": "project_panel::CompareMarkedFiles",
       "shift-find": "project_panel::NewSearchInDirectory",
       "ctrl-alt-shift-f": "project_panel::NewSearchInDirectory",
       "shift-down": "menu::SelectNext",

assets/keymaps/default-macos.json 🔗

@@ -907,6 +907,7 @@
       "cmd-delete": ["project_panel::Delete", { "skip_prompt": false }],
       "alt-cmd-r": "project_panel::RevealInFileManager",
       "ctrl-shift-enter": "project_panel::OpenWithSystem",
+      "alt-d": "project_panel::CompareMarkedFiles",
       "cmd-alt-backspace": ["project_panel::Delete", { "skip_prompt": false }],
       "cmd-alt-shift-f": "project_panel::NewSearchInDirectory",
       "shift-down": "menu::SelectNext",

assets/keymaps/vim.json 🔗

@@ -813,6 +813,7 @@
       "p": "project_panel::Open",
       "x": "project_panel::RevealInFileManager",
       "s": "project_panel::OpenWithSystem",
+      "z d": "project_panel::CompareMarkedFiles",
       "] c": "project_panel::SelectNextGitEntry",
       "[ c": "project_panel::SelectPrevGitEntry",
       "] d": "project_panel::SelectNextDiagnostic",

crates/project_panel/Cargo.toml 🔗

@@ -19,6 +19,7 @@ command_palette_hooks.workspace = true
 db.workspace = true
 editor.workspace = true
 file_icons.workspace = true
+git_ui.workspace = true
 indexmap.workspace = true
 git.workspace = true
 gpui.workspace = true

crates/project_panel/src/project_panel.rs 🔗

@@ -16,6 +16,7 @@ use editor::{
 };
 use file_icons::FileIcons;
 use git::status::GitSummary;
+use git_ui::file_diff_view::FileDiffView;
 use gpui::{
     Action, AnyElement, App, ArcCow, AsyncWindowContext, Bounds, ClipboardItem, Context,
     CursorStyle, DismissEvent, Div, DragMoveEvent, Entity, EventEmitter, ExternalPaths,
@@ -93,7 +94,7 @@ pub struct ProjectPanel {
     unfolded_dir_ids: HashSet<ProjectEntryId>,
     // Currently selected leaf entry (see auto-folding for a definition of that) in a file tree
     selection: Option<SelectedEntry>,
-    marked_entries: BTreeSet<SelectedEntry>,
+    marked_entries: Vec<SelectedEntry>,
     context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
     edit_state: Option<EditState>,
     filename_editor: Entity<Editor>,
@@ -280,6 +281,8 @@ actions!(
         SelectNextDirectory,
         /// Selects the previous directory.
         SelectPrevDirectory,
+        /// Opens a diff view to compare two marked files.
+        CompareMarkedFiles,
     ]
 );
 
@@ -376,7 +379,7 @@ struct DraggedProjectEntryView {
     selection: SelectedEntry,
     details: EntryDetails,
     click_offset: Point<Pixels>,
-    selections: Arc<BTreeSet<SelectedEntry>>,
+    selections: Arc<[SelectedEntry]>,
 }
 
 struct ItemColors {
@@ -442,7 +445,15 @@ impl ProjectPanel {
                     }
                 }
                 project::Event::ActiveEntryChanged(None) => {
-                    this.marked_entries.clear();
+                    let is_active_item_file_diff_view = this
+                        .workspace
+                        .upgrade()
+                        .and_then(|ws| ws.read(cx).active_item(cx))
+                        .map(|item| item.act_as_type(TypeId::of::<FileDiffView>(), cx).is_some())
+                        .unwrap_or(false);
+                    if !is_active_item_file_diff_view {
+                        this.marked_entries.clear();
+                    }
                 }
                 project::Event::RevealInProjectPanel(entry_id) => {
                     if let Some(()) = this
@@ -676,7 +687,7 @@ impl ProjectPanel {
                                 project_panel.update(cx, |project_panel, _| {
                                     let entry = SelectedEntry { worktree_id, entry_id };
                                     project_panel.marked_entries.clear();
-                                    project_panel.marked_entries.insert(entry);
+                                    project_panel.marked_entries.push(entry);
                                     project_panel.selection = Some(entry);
                                 });
                                 if !focus_opened_item {
@@ -887,6 +898,7 @@ impl ProjectPanel {
             let should_hide_rename = is_root
                 && (cfg!(target_os = "windows")
                     || (settings.hide_root && visible_worktrees_count == 1));
+            let should_show_compare = !is_dir && self.file_abs_paths_to_diff(cx).is_some();
 
             let context_menu = ContextMenu::build(window, cx, |menu, _, _| {
                 menu.context(self.focus_handle.clone()).map(|menu| {
@@ -918,6 +930,10 @@ impl ProjectPanel {
                             .when(is_foldable, |menu| {
                                 menu.action("Fold Directory", Box::new(FoldDirectory))
                             })
+                            .when(should_show_compare, |menu| {
+                                menu.separator()
+                                    .action("Compare marked files", Box::new(CompareMarkedFiles))
+                            })
                             .separator()
                             .action("Cut", Box::new(Cut))
                             .action("Copy", Box::new(Copy))
@@ -1262,7 +1278,7 @@ impl ProjectPanel {
             };
             self.selection = Some(selection);
             if window.modifiers().shift {
-                self.marked_entries.insert(selection);
+                self.marked_entries.push(selection);
             }
             self.autoscroll(cx);
             cx.notify();
@@ -2007,7 +2023,7 @@ impl ProjectPanel {
                     };
                     self.selection = Some(selection);
                     if window.modifiers().shift {
-                        self.marked_entries.insert(selection);
+                        self.marked_entries.push(selection);
                     }
 
                     self.autoscroll(cx);
@@ -2244,7 +2260,7 @@ impl ProjectPanel {
                 };
                 self.selection = Some(selection);
                 if window.modifiers().shift {
-                    self.marked_entries.insert(selection);
+                    self.marked_entries.push(selection);
                 }
                 self.autoscroll(cx);
                 cx.notify();
@@ -2572,6 +2588,43 @@ impl ProjectPanel {
         }
     }
 
+    fn file_abs_paths_to_diff(&self, cx: &Context<Self>) -> Option<(PathBuf, PathBuf)> {
+        let mut selections_abs_path = self
+            .marked_entries
+            .iter()
+            .filter_map(|entry| {
+                let project = self.project.read(cx);
+                let worktree = project.worktree_for_id(entry.worktree_id, cx)?;
+                let entry = worktree.read(cx).entry_for_id(entry.entry_id)?;
+                if !entry.is_file() {
+                    return None;
+                }
+                worktree.read(cx).absolutize(&entry.path).ok()
+            })
+            .rev();
+
+        let last_path = selections_abs_path.next()?;
+        let previous_to_last = selections_abs_path.next()?;
+        Some((previous_to_last, last_path))
+    }
+
+    fn compare_marked_files(
+        &mut self,
+        _: &CompareMarkedFiles,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let selected_files = self.file_abs_paths_to_diff(cx);
+        if let Some((file_path1, file_path2)) = selected_files {
+            self.workspace
+                .update(cx, |workspace, cx| {
+                    FileDiffView::open(file_path1, file_path2, workspace, window, cx)
+                        .detach_and_log_err(cx);
+                })
+                .ok();
+        }
+    }
+
     fn open_system(&mut self, _: &OpenWithSystem, _: &mut Window, cx: &mut Context<Self>) {
         if let Some((worktree, entry)) = self.selected_entry(cx) {
             let abs_path = worktree.abs_path().join(&entry.path);
@@ -3914,11 +3967,9 @@ impl ProjectPanel {
 
         let depth = details.depth;
         let worktree_id = details.worktree_id;
-        let selections = Arc::new(self.marked_entries.clone());
-
         let dragged_selection = DraggedSelection {
             active_selection: selection,
-            marked_selections: selections,
+            marked_selections: Arc::from(self.marked_entries.clone()),
         };
 
         let bg_color = if is_marked {
@@ -4089,7 +4140,7 @@ impl ProjectPanel {
                         });
                         if drag_state.items().count() == 1 {
                             this.marked_entries.clear();
-                            this.marked_entries.insert(drag_state.active_selection);
+                            this.marked_entries.push(drag_state.active_selection);
                         }
                         this.hover_expand_task.take();
 
@@ -4156,65 +4207,69 @@ impl ProjectPanel {
                 }),
             )
             .on_click(
-                cx.listener(move |this, event: &gpui::ClickEvent, window, cx| {
+                cx.listener(move |project_panel, event: &gpui::ClickEvent, window, cx| {
                     if event.is_right_click() || event.first_focus()
                         || show_editor
                     {
                         return;
                     }
                     if event.standard_click() {
-                        this.mouse_down = false;
+                        project_panel.mouse_down = false;
                     }
                     cx.stop_propagation();
 
-                    if let Some(selection) = this.selection.filter(|_| event.modifiers().shift) {
-                        let current_selection = this.index_for_selection(selection);
+                    if let Some(selection) = project_panel.selection.filter(|_| event.modifiers().shift) {
+                        let current_selection = project_panel.index_for_selection(selection);
                         let clicked_entry = SelectedEntry {
                             entry_id,
                             worktree_id,
                         };
-                        let target_selection = this.index_for_selection(clicked_entry);
+                        let target_selection = project_panel.index_for_selection(clicked_entry);
                         if let Some(((_, _, source_index), (_, _, target_index))) =
                             current_selection.zip(target_selection)
                         {
                             let range_start = source_index.min(target_index);
                             let range_end = source_index.max(target_index) + 1;
-                            let mut new_selections = BTreeSet::new();
-                            this.for_each_visible_entry(
+                            let mut new_selections = Vec::new();
+                            project_panel.for_each_visible_entry(
                                 range_start..range_end,
                                 window,
                                 cx,
                                 |entry_id, details, _, _| {
-                                    new_selections.insert(SelectedEntry {
+                                    new_selections.push(SelectedEntry {
                                         entry_id,
                                         worktree_id: details.worktree_id,
                                     });
                                 },
                             );
 
-                            this.marked_entries = this
-                                .marked_entries
-                                .union(&new_selections)
-                                .cloned()
-                                .collect();
+                            for selection in &new_selections {
+                                if !project_panel.marked_entries.contains(selection) {
+                                    project_panel.marked_entries.push(*selection);
+                                }
+                            }
 
-                            this.selection = Some(clicked_entry);
-                            this.marked_entries.insert(clicked_entry);
+                            project_panel.selection = Some(clicked_entry);
+                            if !project_panel.marked_entries.contains(&clicked_entry) {
+                                project_panel.marked_entries.push(clicked_entry);
+                            }
                         }
                     } else if event.modifiers().secondary() {
                         if event.click_count() > 1 {
-                            this.split_entry(entry_id, cx);
+                            project_panel.split_entry(entry_id, cx);
                         } else {
-                            this.selection = Some(selection);
-                            if !this.marked_entries.insert(selection) {
-                                this.marked_entries.remove(&selection);
+                            project_panel.selection = Some(selection);
+                            if let Some(position) = project_panel.marked_entries.iter().position(|e| *e == selection) {
+                                project_panel.marked_entries.remove(position);
+                            } else {
+                                project_panel.marked_entries.push(selection);
                             }
                         }
                     } else if kind.is_dir() {
-                        this.marked_entries.clear();
+                        project_panel.marked_entries.clear();
                         if is_sticky {
-                            if let Some((_, _, index)) = this.index_for_entry(entry_id, worktree_id) {
-                                this.scroll_handle.scroll_to_item_with_offset(index, ScrollStrategy::Top, sticky_index.unwrap_or(0));
+                            if let Some((_, _, index)) = project_panel.index_for_entry(entry_id, worktree_id) {
+                                project_panel.scroll_handle.scroll_to_item_with_offset(index, ScrollStrategy::Top, sticky_index.unwrap_or(0));
                                 cx.notify();
                                 // move down by 1px so that clicked item
                                 // don't count as sticky anymore
@@ -4230,16 +4285,16 @@ impl ProjectPanel {
                             }
                         }
                         if event.modifiers().alt {
-                            this.toggle_expand_all(entry_id, window, cx);
+                            project_panel.toggle_expand_all(entry_id, window, cx);
                         } else {
-                            this.toggle_expanded(entry_id, window, cx);
+                            project_panel.toggle_expanded(entry_id, window, cx);
                         }
                     } else {
                         let preview_tabs_enabled = PreviewTabsSettings::get_global(cx).enabled;
                         let click_count = event.click_count();
                         let focus_opened_item = !preview_tabs_enabled || click_count > 1;
                         let allow_preview = preview_tabs_enabled && click_count == 1;
-                        this.open_entry(entry_id, focus_opened_item, allow_preview, cx);
+                        project_panel.open_entry(entry_id, focus_opened_item, allow_preview, cx);
                     }
                 }),
             )
@@ -4810,12 +4865,21 @@ impl ProjectPanel {
         {
             anyhow::bail!("can't reveal an ignored entry in the project panel");
         }
+        let is_active_item_file_diff_view = self
+            .workspace
+            .upgrade()
+            .and_then(|ws| ws.read(cx).active_item(cx))
+            .map(|item| item.act_as_type(TypeId::of::<FileDiffView>(), cx).is_some())
+            .unwrap_or(false);
+        if is_active_item_file_diff_view {
+            return Ok(());
+        }
 
         let worktree_id = worktree.id();
         self.expand_entry(worktree_id, entry_id, cx);
         self.update_visible_entries(Some((worktree_id, entry_id)), cx);
         self.marked_entries.clear();
-        self.marked_entries.insert(SelectedEntry {
+        self.marked_entries.push(SelectedEntry {
             worktree_id,
             entry_id,
         });
@@ -5170,6 +5234,7 @@ impl Render for ProjectPanel {
                 .on_action(cx.listener(Self::unfold_directory))
                 .on_action(cx.listener(Self::fold_directory))
                 .on_action(cx.listener(Self::remove_from_project))
+                .on_action(cx.listener(Self::compare_marked_files))
                 .when(!project.is_read_only(cx), |el| {
                     el.on_action(cx.listener(Self::new_file))
                         .on_action(cx.listener(Self::new_directory))

crates/project_panel/src/project_panel_tests.rs 🔗

@@ -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;

crates/workspace/src/pane.rs 🔗

@@ -62,7 +62,7 @@ pub struct SelectedEntry {
 #[derive(Debug)]
 pub struct DraggedSelection {
     pub active_selection: SelectedEntry,
-    pub marked_selections: Arc<BTreeSet<SelectedEntry>>,
+    pub marked_selections: Arc<[SelectedEntry]>,
 }
 
 impl DraggedSelection {