workspace: Fix read-only pane buttons being clickable even when a no-op (#46065)

Lukas Wirth created

The button was clickable for read-only panes even if the underlying item
is not write-toggleable.

Release Notes:

- N/A *or* Added/Fixed/Improved ...

Change summary

crates/diagnostics/src/buffer_diagnostics.rs |  6 +-
crates/editor/src/editor.rs                  |  8 +++
crates/editor/src/items.rs                   |  4 
crates/language/src/buffer.rs                |  2 
crates/multi_buffer/src/multi_buffer.rs      |  4 +
crates/workspace/src/item.rs                 | 11 ++--
crates/workspace/src/pane.rs                 | 51 +++++++++++----------
7 files changed, 51 insertions(+), 35 deletions(-)

Detailed changes

crates/diagnostics/src/buffer_diagnostics.rs 🔗

@@ -15,7 +15,7 @@ use gpui::{
     InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
     Task, WeakEntity, Window, actions, div,
 };
-use language::{Buffer, DiagnosticEntry, DiagnosticEntryRef, Point};
+use language::{Buffer, Capability, DiagnosticEntry, DiagnosticEntryRef, Point};
 use project::{
     DiagnosticSummary, Event, Project, ProjectItem, ProjectPath,
     project_settings::{DiagnosticSeverity, ProjectSettings},
@@ -763,8 +763,8 @@ impl Item for BufferDiagnosticsEditor {
         self.multibuffer.read(cx).is_dirty(cx)
     }
 
-    fn is_read_only(&self, cx: &App) -> bool {
-        self.multibuffer.read(cx).read_only()
+    fn capability(&self, cx: &App) -> Capability {
+        self.multibuffer.read(cx).capability()
     }
 
     fn navigate(

crates/editor/src/editor.rs 🔗

@@ -3162,6 +3162,14 @@ impl Editor {
         }
     }
 
+    pub fn capability(&self, cx: &App) -> Capability {
+        if self.read_only {
+            Capability::ReadOnly
+        } else {
+            self.buffer.read(cx).capability()
+        }
+    }
+
     pub fn read_only(&self, cx: &App) -> bool {
         self.read_only || self.buffer.read(cx).read_only()
     }

crates/editor/src/items.rs 🔗

@@ -805,8 +805,8 @@ impl Item for Editor {
         self.buffer().read(cx).read(cx).is_dirty()
     }
 
-    fn is_read_only(&self, cx: &App) -> bool {
-        self.read_only(cx)
+    fn capability(&self, cx: &App) -> Capability {
+        self.capability(cx)
     }
 
     // Note: this mirrors the logic in `Editor::toggle_read_only`, but is reachable

crates/language/src/buffer.rs 🔗

@@ -85,7 +85,7 @@ pub static BUFFER_DIFF_TASK: LazyLock<TaskLabel> = LazyLock::new(TaskLabel::new)
 pub enum Capability {
     /// The buffer is a mutable replica.
     ReadWrite,
-    /// The buffer is a mutable replica, but toggled to read-only.
+    /// The buffer is a mutable replica, but toggled to be only readable.
     Read,
     /// The buffer is a read-only replica.
     ReadOnly,

crates/multi_buffer/src/multi_buffer.rs 🔗

@@ -1198,6 +1198,10 @@ impl MultiBuffer {
         !self.capability.editable()
     }
 
+    pub fn capability(&self) -> Capability {
+        self.capability
+    }
+
     /// Returns an up-to-date snapshot of the MultiBuffer.
     #[ztracing::instrument(skip_all)]
     pub fn snapshot(&self, cx: &App) -> MultiBufferSnapshot {

crates/workspace/src/item.rs 🔗

@@ -15,6 +15,7 @@ use gpui::{
     EventEmitter, FocusHandle, Focusable, Font, HighlightStyle, Pixels, Point, Render,
     SharedString, Task, WeakEntity, Window,
 };
+use language::Capability;
 use project::{Project, ProjectEntryId, ProjectPath};
 pub use settings::{
     ActivateOnClose, ClosePosition, RegisterSetting, Settings, SettingsLocation, ShowCloseButton,
@@ -255,8 +256,8 @@ pub trait Item: Focusable + EventEmitter<Self::Event> + Render + Sized {
     fn is_dirty(&self, _: &App) -> bool {
         false
     }
-    fn is_read_only(&self, _: &App) -> bool {
-        false
+    fn capability(&self, _: &App) -> Capability {
+        Capability::ReadWrite
     }
 
     fn toggle_read_only(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
@@ -482,7 +483,7 @@ pub trait ItemHandle: 'static + Send {
     fn item_id(&self) -> EntityId;
     fn to_any_view(&self) -> AnyView;
     fn is_dirty(&self, cx: &App) -> bool;
-    fn is_read_only(&self, cx: &App) -> bool;
+    fn capability(&self, cx: &App) -> Capability;
     fn toggle_read_only(&self, window: &mut Window, cx: &mut App);
     fn has_deleted_file(&self, cx: &App) -> bool;
     fn has_conflict(&self, cx: &App) -> bool;
@@ -957,8 +958,8 @@ impl<T: Item> ItemHandle for Entity<T> {
         self.read(cx).is_dirty(cx)
     }
 
-    fn is_read_only(&self, cx: &App) -> bool {
-        self.read(cx).is_read_only(cx)
+    fn capability(&self, cx: &App) -> Capability {
+        self.read(cx).capability(cx)
     }
 
     fn toggle_read_only(&self, window: &mut Window, cx: &mut App) {

crates/workspace/src/pane.rs 🔗

@@ -26,7 +26,7 @@ use gpui::{
     actions, anchored, deferred, prelude::*,
 };
 use itertools::Itertools;
-use language::DiagnosticSeverity;
+use language::{Capability, DiagnosticSeverity};
 use parking_lot::Mutex;
 use project::{DirectoryLister, Project, ProjectEntryId, ProjectPath, WorktreeId};
 use schemars::JsonSchema;
@@ -2678,12 +2678,13 @@ impl Pane {
         let is_pinned = self.is_tab_pinned(ix);
         let position_relative_to_active_item = ix.cmp(&self.active_item_index);
 
-        let read_only_toggle = || {
+        let read_only_toggle = |toggleable: bool| {
             IconButton::new("toggle_read_only", IconName::FileLock)
                 .size(ButtonSize::None)
                 .shape(IconButtonShape::Square)
                 .icon_color(Color::Muted)
                 .icon_size(IconSize::Small)
+                .disabled(!toggleable)
                 .tooltip(move |_, cx| {
                     Tooltip::with_meta("Unlock File", None, "This will make this file editable", cx)
                 })
@@ -2696,6 +2697,7 @@ impl Pane {
 
         let has_file_icon = icon.is_some() | decorated_icon.is_some();
 
+        let capability = item.capability(cx);
         let tab = Tab::new(ix)
             .position(if is_first_item {
                 TabPosition::First
@@ -2836,21 +2838,21 @@ impl Pane {
                         Some(decorated_icon.into_any_element())
                     } else if let Some(icon) = icon {
                         Some(icon.into_any_element())
-                    } else if item.is_read_only(cx) {
-                        Some(read_only_toggle().into_any_element())
+                    } else if !capability.editable() {
+                        Some(read_only_toggle(capability == Capability::Read).into_any_element())
                     } else {
                         None
                     })
                     .child(label)
                     .map(|this| match tab_tooltip_content {
                         Some(TabTooltipContent::Text(text)) => {
-                            if item.is_read_only(cx) {
+                            if capability.editable() {
+                                this.tooltip(Tooltip::text(text))
+                            } else {
                                 this.tooltip(move |_, cx| {
                                     let text = text.clone();
                                     Tooltip::with_meta(text, None, "Read-Only File", cx)
                                 })
-                            } else {
-                                this.tooltip(Tooltip::text(text))
                             }
                         }
                         Some(TabTooltipContent::Custom(element_fn)) => {
@@ -2858,8 +2860,8 @@ impl Pane {
                         }
                         None => this,
                     })
-                    .when(item.is_read_only(cx) && has_file_icon, |this| {
-                        this.child(read_only_toggle())
+                    .when(capability == Capability::Read && has_file_icon, |this| {
+                        this.child(read_only_toggle(true))
                     }),
             );
 
@@ -2876,7 +2878,6 @@ impl Pane {
         let has_items_to_right = ix < total_items - 1;
         let has_clean_items = self.items.iter().any(|item| !item.is_dirty(cx));
         let is_pinned = self.is_tab_pinned(ix);
-        let is_read_only = item.is_read_only(cx);
 
         let pane = cx.entity().downgrade();
         let menu_context = item.item_focus_handle(cx);
@@ -3028,20 +3029,22 @@ impl Pane {
                             })
                         };
 
-                        let read_only_label = if is_read_only {
-                            "Make File Editable"
-                        } else {
-                            "Make File Read-Only"
-                        };
-                        menu = menu.separator().entry(
-                            read_only_label,
-                            None,
-                            window.handler_for(&pane, move |pane, window, cx| {
-                                if let Some(item) = pane.item_for_index(ix) {
-                                    item.toggle_read_only(window, cx);
-                                }
-                            }),
-                        );
+                        if capability != Capability::ReadOnly {
+                            let read_only_label = if capability.editable() {
+                                "Make File Read-Only"
+                            } else {
+                                "Make File Editable"
+                            };
+                            menu = menu.separator().entry(
+                                read_only_label,
+                                None,
+                                window.handler_for(&pane, move |pane, window, cx| {
+                                    if let Some(item) = pane.item_for_index(ix) {
+                                        item.toggle_read_only(window, cx);
+                                    }
+                                }),
+                            );
+                        }
 
                         if let Some(entry) = single_entry_to_resolve {
                             let project_path = pane