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

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 ...
This commit is contained in:
Lukas Wirth authored and GitHub committed 2026-01-05 10:27:46 +00:00
1 parent e57285fe39
commit 9cdb986fd4
7 files changed
+51 -35

No files matched your search

+3 -3
View File
@@ -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(
+8
View File
@@ -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()
}
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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,
+4
View File
@@ -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 {
+6 -5
View File
@@ -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) {
+27 -24
View File
@@ -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