editor: Prevent auto save formatting on read only files (#62921)
# Objective Read-only marked files in zed could still be affected by format on save feature. This PR fixes that. FIXES #62899 ## Solution These changes add an early return on Editor's `Item::save` implementation so neither formatting nor the write to disk occurs. ## Testing I've tested by reproducing the issue in the original ticket. ## Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] 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) - [] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable --- Release Notes: - Fixed read-only files being formatted and saved when `format_on_save` is enabled --------- Co-authored-by: Kirill Bulatov <kirill@zed.dev>
This commit is contained in:
1 parent
fbe49339af
commit
5d473cc6af
7 files changed
+449
-20
No files matched your search
@@ -8223,8 +8223,11 @@ impl Editor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
if self.read_only(cx) {
|
||||
return Task::ready(Ok(()));
|
||||
}
|
||||
let buffer = self.buffer.clone();
|
||||
let (buffers, target) = match target {
|
||||
let (mut buffers, target) = match target {
|
||||
FormatTarget::Buffers(buffers) => (buffers, LspFormatTarget::Buffers),
|
||||
FormatTarget::Ranges(selection_ranges) => {
|
||||
let multi_buffer = buffer.read(cx);
|
||||
@@ -8250,6 +8253,8 @@ impl Editor {
|
||||
}
|
||||
};
|
||||
|
||||
buffers.retain(|buffer| !buffer.read(cx).read_only());
|
||||
|
||||
let transaction_id_prev = buffer.read(cx).last_transaction_id(cx);
|
||||
let selections_prev = transaction_id_prev
|
||||
.and_then(|transaction_id_prev| {
|
||||
@@ -9817,6 +9822,10 @@ impl Editor {
|
||||
cx.emit(EditorEvent::TitleChanged);
|
||||
cx.emit(EditorEvent::FileHandleChanged);
|
||||
}
|
||||
multi_buffer::Event::CapabilityChanged => {
|
||||
cx.emit(EditorEvent::CapabilityChanged);
|
||||
cx.notify();
|
||||
}
|
||||
multi_buffer::Event::Reloaded | multi_buffer::Event::BufferDiffChanged => {
|
||||
cx.emit(EditorEvent::TitleChanged)
|
||||
}
|
||||
@@ -11960,6 +11969,7 @@ pub enum EditorEvent {
|
||||
Blurred,
|
||||
DirtyChanged,
|
||||
Saved,
|
||||
CapabilityChanged,
|
||||
TitleChanged,
|
||||
FileHandleChanged,
|
||||
SelectionsChanged {
|
||||
|
||||
@@ -26,7 +26,7 @@ use gpui::{
|
||||
use indoc::indoc;
|
||||
use language::{
|
||||
BracketPair, BracketPairConfig,
|
||||
Capability::ReadWrite,
|
||||
Capability::{Read, ReadOnly, ReadWrite},
|
||||
ContextLocation, ContextProvider, DiagnosticSourceKind, FakeLspAdapter, IndentGuideSettings,
|
||||
LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName, LanguageQueries,
|
||||
LanguageToolchainStore, Override, PLAIN_TEXT, Point,
|
||||
@@ -15441,6 +15441,344 @@ async fn test_snippet_with_multi_word_prefix(cx: &mut TestAppContext) {
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_read_only_buffer_is_not_formatted_or_saved(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_file(path!("/file.rs"), b"one\ntwo\nthree\n".to_vec())
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/file.rs").as_ref()], cx).await;
|
||||
|
||||
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
|
||||
language_registry.add(rust_lang());
|
||||
let format_request_count = Arc::new(AtomicUsize::new(0));
|
||||
let mut fake_servers = language_registry.register_fake_lsp(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
capabilities: lsp::ServerCapabilities {
|
||||
document_formatting_provider: Some(lsp::OneOf::Left(true)),
|
||||
..lsp::ServerCapabilities::default()
|
||||
},
|
||||
initializer: Some(Box::new({
|
||||
let format_request_count = format_request_count.clone();
|
||||
move |fake_server| {
|
||||
let format_request_count = format_request_count.clone();
|
||||
fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
|
||||
move |_, _| {
|
||||
let format_request_count = format_request_count.clone();
|
||||
async move {
|
||||
format_request_count.fetch_add(1, atomic::Ordering::Release);
|
||||
Ok(Some(vec![lsp::TextEdit::new(
|
||||
lsp::Range::new(
|
||||
lsp::Position::new(0, 3),
|
||||
lsp::Position::new(1, 0),
|
||||
),
|
||||
", ".to_string(),
|
||||
)]))
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
})),
|
||||
..FakeLspAdapter::default()
|
||||
},
|
||||
);
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer(path!("/file.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
buffer.update(cx, |buffer, cx| buffer.set_capability(Read, cx));
|
||||
|
||||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let (editor, cx) = cx.add_window_view(|window, cx| {
|
||||
build_editor_with_project(project.clone(), buffer, window, cx)
|
||||
});
|
||||
|
||||
fake_servers.next().await.unwrap();
|
||||
|
||||
let save = editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor.save(
|
||||
SaveOptions {
|
||||
format: true,
|
||||
force_format: false,
|
||||
autosave: false,
|
||||
},
|
||||
project.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
save.await;
|
||||
|
||||
assert_eq!(format_request_count.load(atomic::Ordering::Acquire), 0);
|
||||
assert_eq!(
|
||||
editor.update(cx, |editor, cx| editor.text(cx)),
|
||||
"one\ntwo\nthree\n"
|
||||
);
|
||||
assert_eq!(
|
||||
fs.load(path!("/file.rs").as_ref()).await.unwrap(),
|
||||
"one\ntwo\nthree\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_read_only_buffer_can_be_saved_as(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/dir"),
|
||||
json!({
|
||||
"file.rs": "one\ntwo\nthree\n",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
|
||||
let worktree_id = project.update(cx, |project, cx| {
|
||||
project.worktrees(cx).next().unwrap().read(cx).id()
|
||||
});
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer(path!("/dir/file.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
buffer.update(cx, |buffer, cx| buffer.set_capability(Read, cx));
|
||||
|
||||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let (editor, cx) = cx.add_window_view(|window, cx| {
|
||||
build_editor_with_project(project.clone(), buffer, window, cx)
|
||||
});
|
||||
|
||||
let save_as = editor.update_in(cx, |editor, window, cx| {
|
||||
editor.save_as(
|
||||
project.clone(),
|
||||
ProjectPath {
|
||||
worktree_id,
|
||||
path: rel_path("copy.rs").into(),
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
save_as.await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
fs.load(path!("/dir/copy.rs").as_ref()).await.unwrap(),
|
||||
"one\ntwo\nthree\n"
|
||||
);
|
||||
assert_eq!(
|
||||
fs.load(path!("/dir/file.rs").as_ref()).await.unwrap(),
|
||||
"one\ntwo\nthree\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_format_and_save_skip_read_only_buffers_in_multi_buffer(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/a"),
|
||||
json!({
|
||||
"main.rs": "one\ntwo\nthree\n",
|
||||
"other.rs": "one\ntwo\nthree\n",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/a").as_ref()], cx).await;
|
||||
|
||||
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
|
||||
language_registry.add(rust_lang());
|
||||
let formatted_uris = Arc::new(Mutex::new(Vec::new()));
|
||||
let mut fake_servers = language_registry.register_fake_lsp(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
capabilities: lsp::ServerCapabilities {
|
||||
document_formatting_provider: Some(lsp::OneOf::Left(true)),
|
||||
..lsp::ServerCapabilities::default()
|
||||
},
|
||||
initializer: Some(Box::new({
|
||||
let formatted_uris = formatted_uris.clone();
|
||||
move |fake_server| {
|
||||
let formatted_uris = formatted_uris.clone();
|
||||
fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
|
||||
move |params, _| {
|
||||
let formatted_uris = formatted_uris.clone();
|
||||
async move {
|
||||
formatted_uris.lock().push(params.text_document.uri.clone());
|
||||
Ok(Some(vec![lsp::TextEdit::new(
|
||||
lsp::Range::new(
|
||||
lsp::Position::new(0, 3),
|
||||
lsp::Position::new(1, 0),
|
||||
),
|
||||
", ".to_string(),
|
||||
)]))
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
})),
|
||||
..FakeLspAdapter::default()
|
||||
},
|
||||
);
|
||||
|
||||
let writable_buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer(path!("/a/main.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let read_only_buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer(path!("/a/other.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
read_only_buffer.update(cx, |buffer, cx| buffer.set_capability(Read, cx));
|
||||
|
||||
let multi_buffer = cx.new(|cx| {
|
||||
let mut multi_buffer = MultiBuffer::new(ReadWrite);
|
||||
multi_buffer.set_excerpts_for_path(
|
||||
PathKey::sorted(0),
|
||||
writable_buffer.clone(),
|
||||
[Point::new(0, 0)..Point::new(3, 0)],
|
||||
0,
|
||||
cx,
|
||||
);
|
||||
multi_buffer.set_excerpts_for_path(
|
||||
PathKey::sorted(1),
|
||||
read_only_buffer.clone(),
|
||||
[Point::new(0, 0)..Point::new(3, 0)],
|
||||
0,
|
||||
cx,
|
||||
);
|
||||
multi_buffer
|
||||
});
|
||||
let (editor, cx) = cx.add_window_view(|window, cx| {
|
||||
build_editor_with_project(project.clone(), multi_buffer, window, cx)
|
||||
});
|
||||
|
||||
fake_servers.next().await.unwrap();
|
||||
|
||||
let format = editor
|
||||
.update_in(cx, |editor, window, cx| editor.format(&Format, window, cx))
|
||||
.unwrap();
|
||||
format.await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
*formatted_uris.lock(),
|
||||
vec![lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap()]
|
||||
);
|
||||
writable_buffer.read_with(cx, |buffer, _| {
|
||||
assert_eq!(buffer.text(), "one, two\nthree\n");
|
||||
});
|
||||
read_only_buffer.read_with(cx, |buffer, _| {
|
||||
assert_eq!(buffer.text(), "one\ntwo\nthree\n");
|
||||
});
|
||||
|
||||
read_only_buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit([(0..0, "dirty ")], None, cx);
|
||||
});
|
||||
assert_eq!(
|
||||
read_only_buffer.read_with(cx, |buffer, _| buffer.is_dirty()),
|
||||
true
|
||||
);
|
||||
|
||||
let save = editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor.save(
|
||||
SaveOptions {
|
||||
format: false,
|
||||
force_format: false,
|
||||
autosave: false,
|
||||
},
|
||||
project.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.unwrap();
|
||||
save.await;
|
||||
|
||||
assert_eq!(
|
||||
fs.load(path!("/a/main.rs").as_ref()).await.unwrap(),
|
||||
"one, two\nthree\n"
|
||||
);
|
||||
assert_eq!(
|
||||
fs.load(path!("/a/other.rs").as_ref()).await.unwrap(),
|
||||
"one\ntwo\nthree\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_save_actions_are_hidden_for_read_only_files(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_file(path!("/file.rs"), b"one\ntwo\nthree\n".to_vec())
|
||||
.await;
|
||||
let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
|
||||
let window = cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
||||
let cx = &mut VisualTestContext::from_window(*window, cx);
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer(path!("/file.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
|
||||
let editor = cx.new_window_entity(|window, cx| {
|
||||
Editor::new(
|
||||
EditorMode::full(),
|
||||
multi_buffer,
|
||||
Some(project.clone()),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
window
|
||||
.update(cx, |multi_workspace, window, cx| {
|
||||
multi_workspace.workspace().update(cx, |workspace, cx| {
|
||||
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
|
||||
})
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
fn save_actions_visible(cx: &mut VisualTestContext) -> (bool, bool, bool, bool) {
|
||||
let actions = cx.update(|window, cx| window.available_actions(cx));
|
||||
let visible = |name: &str| actions.iter().any(|action| action.name() == name);
|
||||
(
|
||||
visible("workspace::Save"),
|
||||
visible("workspace::FormatAndSave"),
|
||||
visible("workspace::SaveWithoutFormat"),
|
||||
visible("workspace::SaveAs"),
|
||||
)
|
||||
}
|
||||
|
||||
assert_eq!(save_actions_visible(cx), (true, true, true, true));
|
||||
|
||||
buffer.update(cx, |buffer, cx| buffer.set_capability(ReadOnly, cx));
|
||||
cx.run_until_parked();
|
||||
assert_eq!(save_actions_visible(cx), (false, false, false, true));
|
||||
|
||||
buffer.update(cx, |buffer, cx| buffer.set_capability(ReadWrite, cx));
|
||||
cx.run_until_parked();
|
||||
assert_eq!(save_actions_visible(cx), (true, true, true, true));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_document_format_during_save(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
@@ -932,6 +932,9 @@ impl Item for Editor {
|
||||
}
|
||||
|
||||
fn can_save(&self, cx: &App) -> bool {
|
||||
if self.read_only(cx) {
|
||||
return false;
|
||||
}
|
||||
let buffer = &self.buffer().read(cx);
|
||||
if let Some(buffer) = buffer.as_singleton() {
|
||||
buffer.read(cx).project_path(cx).is_some()
|
||||
@@ -947,6 +950,9 @@ impl Item for Editor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
if self.read_only(cx) {
|
||||
return Task::ready(Ok(()));
|
||||
}
|
||||
// Add meta data tracking # of auto saves
|
||||
if options.autosave {
|
||||
self.report_editor_event(ReportEditorEvent::Saved { auto_saved: true }, None, cx);
|
||||
@@ -970,7 +976,7 @@ impl Item for Editor {
|
||||
// `save_as`. Trying to save it here errors and aborts the whole save.
|
||||
.filter(|buffer| {
|
||||
let buffer = buffer.read(cx);
|
||||
buffer.is_dirty() && buffer.file().is_some()
|
||||
buffer.is_dirty() && !buffer.read_only() && buffer.file().is_some()
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
@@ -1180,7 +1186,7 @@ impl Item for Editor {
|
||||
f(ItemEvent::UpdateBreadcrumbs);
|
||||
}
|
||||
|
||||
EditorEvent::DirtyChanged => {
|
||||
EditorEvent::DirtyChanged | EditorEvent::CapabilityChanged => {
|
||||
f(ItemEvent::UpdateTab);
|
||||
}
|
||||
|
||||
|
||||
@@ -117,6 +117,7 @@ pub enum Event {
|
||||
transaction_id: TransactionId,
|
||||
},
|
||||
Reloaded,
|
||||
CapabilityChanged,
|
||||
LanguageChanged(BufferId, bool),
|
||||
Reparsed(BufferId),
|
||||
Saved,
|
||||
@@ -2012,7 +2013,7 @@ impl MultiBuffer {
|
||||
BufferEvent::DiagnosticsUpdated => Event::DiagnosticsUpdated,
|
||||
BufferEvent::CapabilityChanged => {
|
||||
self.capability = buffer.read(cx).capability();
|
||||
return;
|
||||
Event::CapabilityChanged
|
||||
}
|
||||
BufferEvent::Operation { .. } | BufferEvent::ReloadNeeded => return,
|
||||
});
|
||||
|
||||
@@ -297,6 +297,7 @@ pub trait Item: Focusable + EventEmitter<Self::Event> + Render + Sized {
|
||||
fn can_save_as(&self, _: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn save(
|
||||
&mut self,
|
||||
_options: SaveOptions,
|
||||
@@ -1437,6 +1438,7 @@ pub mod test {
|
||||
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Task, WeakEntity,
|
||||
Window,
|
||||
};
|
||||
use language::Capability;
|
||||
use project::{Project, ProjectEntryId, ProjectPath, WorktreeId};
|
||||
use std::{any::Any, cell::Cell, sync::Arc};
|
||||
use util::rel_path::rel_path;
|
||||
@@ -1458,6 +1460,7 @@ pub mod test {
|
||||
pub buffer_kind: ItemBufferKind,
|
||||
pub has_conflict: bool,
|
||||
pub has_deleted_file: bool,
|
||||
pub capability: Capability,
|
||||
pub project_items: Vec<Entity<TestProjectItem>>,
|
||||
pub nav_history: Option<ItemNavHistory>,
|
||||
pub tab_descriptions: Option<Vec<&'static str>>,
|
||||
@@ -1548,6 +1551,7 @@ pub mod test {
|
||||
is_dirty: false,
|
||||
has_conflict: false,
|
||||
has_deleted_file: false,
|
||||
capability: Capability::ReadWrite,
|
||||
project_items: Vec::new(),
|
||||
buffer_kind: ItemBufferKind::Singleton,
|
||||
nav_history: None,
|
||||
@@ -1590,6 +1594,11 @@ pub mod test {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_capability(mut self, capability: Capability) -> Self {
|
||||
self.capability = capability;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_project_items(mut self, items: &[Entity<TestProjectItem>]) -> Self {
|
||||
self.project_items.clear();
|
||||
self.project_items.extend(items.iter().cloned());
|
||||
@@ -1737,6 +1746,7 @@ pub mod test {
|
||||
buffer_kind: self.buffer_kind,
|
||||
has_conflict: self.has_conflict,
|
||||
has_deleted_file: self.has_deleted_file,
|
||||
capability: self.capability,
|
||||
project_items: self.project_items.clone(),
|
||||
nav_history: None,
|
||||
tab_descriptions: None,
|
||||
@@ -1777,6 +1787,10 @@ pub mod test {
|
||||
self.buffer_kind == ItemBufferKind::Singleton
|
||||
}
|
||||
|
||||
fn capability(&self, _: &App) -> Capability {
|
||||
self.capability
|
||||
}
|
||||
|
||||
fn save(
|
||||
&mut self,
|
||||
_: SaveOptions,
|
||||
|
||||
@@ -2269,6 +2269,14 @@ impl Pane {
|
||||
return Ok(true);
|
||||
};
|
||||
|
||||
if (save_intent == SaveIntent::Save
|
||||
|| save_intent == SaveIntent::FormatAndSave
|
||||
|| save_intent == SaveIntent::SaveWithoutFormat)
|
||||
&& cx.update(|_window, cx| !item.capability(cx).editable())?
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
let (
|
||||
mut has_conflict,
|
||||
mut is_dirty,
|
||||
@@ -8941,6 +8949,45 @@ mod tests {
|
||||
assert_item_labels(&pane, ["A", "B", "C*"], cx);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_save_intents_are_noops_for_read_only_items(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs, None, cx).await;
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||
|
||||
let item = pane.update_in(cx, |pane, window, cx| {
|
||||
let item = cx.new(|cx| {
|
||||
TestItem::new(cx)
|
||||
.with_dirty(true)
|
||||
.with_capability(Capability::ReadOnly)
|
||||
.with_project_items(&[TestProjectItem::new(1, "read_only.txt", cx)])
|
||||
});
|
||||
pane.add_item(Box::new(item.clone()), true, true, None, window, cx);
|
||||
item
|
||||
});
|
||||
|
||||
for save_intent in [
|
||||
SaveIntent::Save,
|
||||
SaveIntent::FormatAndSave,
|
||||
SaveIntent::SaveWithoutFormat,
|
||||
] {
|
||||
workspace
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
workspace.save_active_item(save_intent, window, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
item.read_with(cx, |item, _| {
|
||||
assert_eq!(item.save_count, 0);
|
||||
assert_eq!(item.save_as_count, 0);
|
||||
});
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
|
||||
@@ -5789,6 +5789,7 @@ impl Workspace {
|
||||
pane::Event::ChangeItemTitle => {
|
||||
if *pane == self.active_pane {
|
||||
self.active_item_path_changed(false, window, cx);
|
||||
cx.notify();
|
||||
}
|
||||
serialize_workspace = false;
|
||||
}
|
||||
@@ -7593,6 +7594,10 @@ impl Workspace {
|
||||
|
||||
/// Multiworkspace uses this to add workspace action handling to itself
|
||||
pub fn actions(&self, div: Div, window: &mut Window, cx: &mut Context<Self>) -> Div {
|
||||
let active_item_is_read_only = self
|
||||
.active_item(cx)
|
||||
.is_some_and(|item| !item.capability(cx).editable());
|
||||
|
||||
self.add_workspace_actions_listeners(div, window, cx)
|
||||
.on_action(cx.listener(
|
||||
|_workspace, action_sequence: &settings::ActionSequence, window, cx| {
|
||||
@@ -7618,21 +7623,29 @@ impl Workspace {
|
||||
let pane = workspace.active_pane().clone();
|
||||
workspace.unfollow_in_pane(&pane, window, cx);
|
||||
}))
|
||||
.on_action(cx.listener(|workspace, action: &Save, window, cx| {
|
||||
workspace
|
||||
.save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), window, cx)
|
||||
.detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
|
||||
}))
|
||||
.on_action(cx.listener(|workspace, _: &FormatAndSave, window, cx| {
|
||||
workspace
|
||||
.save_active_item(SaveIntent::FormatAndSave, window, cx)
|
||||
.detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
|
||||
}))
|
||||
.on_action(cx.listener(|workspace, _: &SaveWithoutFormat, window, cx| {
|
||||
workspace
|
||||
.save_active_item(SaveIntent::SaveWithoutFormat, window, cx)
|
||||
.detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
|
||||
}))
|
||||
.when(!active_item_is_read_only, |this| {
|
||||
this.on_action(cx.listener(|workspace, action: &Save, window, cx| {
|
||||
workspace
|
||||
.save_active_item(
|
||||
action.save_intent.unwrap_or(SaveIntent::Save),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
|
||||
}))
|
||||
.on_action(cx.listener(|workspace, _: &FormatAndSave, window, cx| {
|
||||
workspace
|
||||
.save_active_item(SaveIntent::FormatAndSave, window, cx)
|
||||
.detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
|
||||
}))
|
||||
.on_action(cx.listener(
|
||||
|workspace, _: &SaveWithoutFormat, window, cx| {
|
||||
workspace
|
||||
.save_active_item(SaveIntent::SaveWithoutFormat, window, cx)
|
||||
.detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None);
|
||||
},
|
||||
))
|
||||
})
|
||||
.on_action(cx.listener(|workspace, _: &SaveAs, window, cx| {
|
||||
workspace
|
||||
.save_active_item(SaveIntent::SaveAs, window, cx)
|
||||
|
||||
Reference in new issue
Block a user