From ca2624960ff9010246fb37369f4789e9e4bc68cf Mon Sep 17 00:00:00 2001 From: Dino Date: Thu, 16 Jul 2026 12:45:03 +0100 Subject: [PATCH] project_panel: Disable undo and redo on read-only projects (#61104) # Objective Users in a read-only project should have no permission to create, rename, move, trash or delete files, as such, they should also have no way to undo or redo these actions. These changes ensure that the listeners for both `project_panel::Undo` and `project_panel::Redo` are only registered if the project is not read-only. ## Solution The `ProjectPanel` listeners for both the `Undo` and `Redo` actions were not gated behind the existing `!Project::is_read_only` call, on `ProjectPanel::render`, so these changes move the listeners inside that check. ## Testing Currently, there isn't an easy way to manually test this, as these actions were already disabled in collab and the easiest way to get into a read-only project is connect as a collab guest. However, in order to ensure this continues being asserted once we remove the collab restriction, the `project_panel::tests::undo::undo_redo_unavailable_for_read_only_collab_guest` test has been added. ## Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [ ] Unsafe blocks (if any) have justifying comments - [ ] The content adheres to Zed's UI standards ([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) and [icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md) guidelines) - [x] Tests cover the new/changed behavior - [ ] Performance impact has been considered and is acceptable --- Release Notes: - N/A --- crates/project_panel/src/project_panel.rs | 12 +++---- crates/project_panel/src/tests/undo.rs | 41 +++++++++++++++++++++++ 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index dc15dfbae8..c621c47156 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -6880,6 +6880,7 @@ impl Render for ProjectPanel { // version that understands these messages. let is_collab = project.is_via_collab(); let is_local = project.is_local(); + let supports_undo = cx.has_flag::(); if has_worktree { let item_count = self @@ -7011,13 +7012,6 @@ impl Render for ProjectPanel { .on_action(cx.listener(Self::fold_directory)) .on_action(cx.listener(Self::remove_from_project)) .on_action(cx.listener(Self::compare_marked_files)) - .when( - !is_collab && cx.has_flag::(), - |el| { - el.on_action(cx.listener(Self::undo)) - .on_action(cx.listener(Self::redo)) - }, - ) .when(!project.is_read_only(cx), |el| { el.on_action(cx.listener(Self::new_file)) .on_action(cx.listener(Self::new_directory)) @@ -7031,6 +7025,10 @@ impl Render for ProjectPanel { .on_action(cx.listener(Self::add_to_gitignore)) .on_action(cx.listener(Self::add_to_git_info_exclude)) .when(!is_collab, |el| el.on_action(cx.listener(Self::trash))) + .when(!is_collab && supports_undo, |el| { + el.on_action(cx.listener(Self::undo)) + .on_action(cx.listener(Self::redo)) + }) }) .when( project.is_local() || project.is_via_wsl_with_host_interop(cx), diff --git a/crates/project_panel/src/tests/undo.rs b/crates/project_panel/src/tests/undo.rs index 774a71b6d0..9f607d9c07 100644 --- a/crates/project_panel/src/tests/undo.rs +++ b/crates/project_panel/src/tests/undo.rs @@ -1,5 +1,6 @@ #![cfg(test)] +use client::proto; use collections::HashSet; use fs::{FakeFs, Fs}; use gpui::{Entity, VisualTestContext}; @@ -240,6 +241,12 @@ impl TestContext { .unwrap(); let mut cx = VisualTestContext::from_window(window.into(), cx); let panel = workspace.update_in(&mut cx, ProjectPanel::new); + + workspace.update_in(&mut cx, |workspace, window, cx| { + workspace.add_panel(panel.clone(), window, cx); + workspace.focus_panel::(window, cx); + }); + cx.run_until_parked(); TestContext { panel, fs, cx } @@ -458,3 +465,37 @@ async fn record_via_collab(cx: &mut gpui::TestAppContext) { cx.redo().await; cx.assert_fs_state_is(&["b.txt", "renamed.txt"]); } + +#[gpui::test] +async fn undo_redo_unavailable_for_read_only_collab_guest(cx: &mut gpui::TestAppContext) { + let mut cx = TestContext::new(cx).await; + let focus_handle = cx + .panel + .read_with(&cx.cx, |panel, _| panel.focus_handle.clone()); + + cx.cx.update(|window, _cx| { + assert!(window.is_action_available_in(&crate::Undo, &focus_handle)); + assert!(window.is_action_available_in(&crate::Redo, &focus_handle)); + }); + + // In order to simulate a read-only project, we mark it both as a collab + // session as well as being a guest, which only has read access. + // This is currently a bit redundant, seeing as these actions are already + // disabled in collab either way. However, we'll want to enable undo/redo in + // collab in the future and this test will ensure that, at that point, we + // continue to not allow undo/redo in read-only projects. + cx.panel.update(&mut cx.cx, |panel, cx| { + panel.project.update(cx, |project, cx| { + project.mark_as_collab_for_testing(); + project.set_role(proto::ChannelRole::Guest, cx); + }); + + assert!(panel.project.read(cx).is_read_only(cx)); + cx.notify(); + }); + + cx.cx.update(|window, _cx| { + assert!(!window.is_action_available_in(&crate::Undo, &focus_handle)); + assert!(!window.is_action_available_in(&crate::Redo, &focus_handle)); + }); +}