mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-28 19:02:35 +03:00
Merge pull request #1027 from zed-industries/missing-menu-commands
Add missing File menu commands, improve handling of unsaved multibuffers
This commit is contained in:
commit
acf9a59cc2
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -1245,6 +1245,7 @@ dependencies = [
|
||||
"project",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smallvec",
|
||||
"theme",
|
||||
"unindent",
|
||||
"util",
|
||||
@ -4149,6 +4150,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smallvec",
|
||||
"theme",
|
||||
"unindent",
|
||||
"util",
|
||||
|
@ -14,13 +14,16 @@
|
||||
"shift-cmd-{": "pane::ActivatePrevItem",
|
||||
"shift-cmd-}": "pane::ActivateNextItem",
|
||||
"cmd-w": "pane::CloseActiveItem",
|
||||
"cmd-shift-W": "workspace::CloseWindow",
|
||||
"alt-cmd-w": "pane::CloseInactiveItems",
|
||||
"cmd-s": "workspace::Save",
|
||||
"cmd-shift-S": "workspace::SaveAs",
|
||||
"cmd-=": "zed::IncreaseBufferFontSize",
|
||||
"cmd--": "zed::DecreaseBufferFontSize",
|
||||
"cmd-,": "zed::OpenSettings",
|
||||
"cmd-q": "zed::Quit",
|
||||
"cmd-n": "workspace::OpenNew",
|
||||
"cmd-n": "workspace::NewFile",
|
||||
"cmd-shift-N": "workspace::NewWindow",
|
||||
"cmd-o": "workspace::Open"
|
||||
}
|
||||
},
|
||||
@ -217,7 +220,8 @@
|
||||
"cmd-t": "project_symbols::Toggle",
|
||||
"cmd-p": "file_finder::Toggle",
|
||||
"cmd-shift-P": "command_palette::Toggle",
|
||||
"cmd-shift-M": "diagnostics::Deploy"
|
||||
"cmd-shift-M": "diagnostics::Deploy",
|
||||
"cmd-alt-s": "workspace::SaveAll"
|
||||
}
|
||||
},
|
||||
// Bindings from Sublime Text
|
||||
|
@ -9,6 +9,7 @@ doctest = false
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0"
|
||||
smallvec = { version = "1.6", features = ["union"] }
|
||||
collections = { path = "../collections" }
|
||||
editor = { path = "../editor" }
|
||||
language = { path = "../language" }
|
||||
|
@ -18,6 +18,7 @@ use language::{
|
||||
use project::{DiagnosticSummary, Project, ProjectPath};
|
||||
use serde_json::json;
|
||||
use settings::Settings;
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
cmp::Ordering,
|
||||
@ -479,8 +480,12 @@ impl workspace::Item for ProjectDiagnosticsEditor {
|
||||
None
|
||||
}
|
||||
|
||||
fn project_entry_id(&self, _: &AppContext) -> Option<project::ProjectEntryId> {
|
||||
None
|
||||
fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[project::ProjectEntryId; 3]> {
|
||||
self.editor.project_entry_ids(cx)
|
||||
}
|
||||
|
||||
fn is_singleton(&self, _: &AppContext) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn navigate(&mut self, data: Box<dyn Any>, cx: &mut ViewContext<Self>) -> bool {
|
||||
@ -516,10 +521,6 @@ impl workspace::Item for ProjectDiagnosticsEditor {
|
||||
self.editor.reload(project, cx)
|
||||
}
|
||||
|
||||
fn can_save_as(&self, _: &AppContext) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn save_as(
|
||||
&mut self,
|
||||
_: ModelHandle<Project>,
|
||||
|
@ -216,7 +216,7 @@ pub enum Direction {
|
||||
}
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(Editor::open_new);
|
||||
cx.add_action(Editor::new_file);
|
||||
cx.add_action(|this: &mut Editor, action: &Scroll, cx| this.set_scroll_position(action.0, cx));
|
||||
cx.add_action(Editor::select);
|
||||
cx.add_action(Editor::cancel);
|
||||
@ -1002,9 +1002,9 @@ impl Editor {
|
||||
this
|
||||
}
|
||||
|
||||
pub fn open_new(
|
||||
pub fn new_file(
|
||||
workspace: &mut Workspace,
|
||||
_: &workspace::OpenNew,
|
||||
_: &workspace::NewFile,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
let project = workspace.project().clone();
|
||||
|
@ -9,6 +9,7 @@ use language::{Bias, Buffer, File as _, SelectionGoal};
|
||||
use project::{File, Project, ProjectEntryId, ProjectPath};
|
||||
use rpc::proto::{self, update_view};
|
||||
use settings::Settings;
|
||||
use smallvec::SmallVec;
|
||||
use std::{fmt::Write, path::PathBuf, time::Duration};
|
||||
use text::{Point, Selection};
|
||||
use util::TryFutureExt;
|
||||
@ -293,14 +294,25 @@ impl Item for Editor {
|
||||
}
|
||||
|
||||
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath> {
|
||||
File::from_dyn(self.buffer().read(cx).file(cx)).map(|file| ProjectPath {
|
||||
let buffer = self.buffer.read(cx).as_singleton()?;
|
||||
let file = buffer.read(cx).file();
|
||||
File::from_dyn(file).map(|file| ProjectPath {
|
||||
worktree_id: file.worktree_id(cx),
|
||||
path: file.path().clone(),
|
||||
})
|
||||
}
|
||||
|
||||
fn project_entry_id(&self, cx: &AppContext) -> Option<ProjectEntryId> {
|
||||
File::from_dyn(self.buffer().read(cx).file(cx)).and_then(|file| file.project_entry_id(cx))
|
||||
fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]> {
|
||||
self.buffer
|
||||
.read(cx)
|
||||
.files(cx)
|
||||
.into_iter()
|
||||
.filter_map(|file| File::from_dyn(Some(file))?.project_entry_id(cx))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn is_singleton(&self, cx: &AppContext) -> bool {
|
||||
self.buffer.read(cx).is_singleton()
|
||||
}
|
||||
|
||||
fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
|
||||
@ -372,10 +384,6 @@ impl Item for Editor {
|
||||
})
|
||||
}
|
||||
|
||||
fn can_save_as(&self, cx: &AppContext) -> bool {
|
||||
self.buffer().read(cx).is_singleton()
|
||||
}
|
||||
|
||||
fn save_as(
|
||||
&mut self,
|
||||
project: ModelHandle<Project>,
|
||||
|
@ -12,6 +12,7 @@ use language::{
|
||||
ToPointUtf16 as _, TransactionId,
|
||||
};
|
||||
use settings::Settings;
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
cell::{Ref, RefCell},
|
||||
cmp, fmt, io,
|
||||
@ -1126,18 +1127,26 @@ impl MultiBuffer {
|
||||
.and_then(|(buffer, _)| buffer.read(cx).language())
|
||||
}
|
||||
|
||||
pub fn file<'a>(&self, cx: &'a AppContext) -> Option<&'a dyn File> {
|
||||
self.as_singleton()?.read(cx).file()
|
||||
pub fn files<'a>(&'a self, cx: &'a AppContext) -> SmallVec<[&'a dyn File; 2]> {
|
||||
let buffers = self.buffers.borrow();
|
||||
buffers
|
||||
.values()
|
||||
.filter_map(|buffer| buffer.buffer.read(cx).file())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn title(&self, cx: &AppContext) -> String {
|
||||
if let Some(title) = self.title.clone() {
|
||||
title
|
||||
} else if let Some(file) = self.file(cx) {
|
||||
file.file_name(cx).to_string_lossy().into()
|
||||
} else {
|
||||
"untitled".into()
|
||||
return title;
|
||||
}
|
||||
|
||||
if let Some(buffer) = self.as_singleton() {
|
||||
if let Some(file) = buffer.read(cx).file() {
|
||||
return file.file_name(cx).to_string_lossy().into();
|
||||
}
|
||||
}
|
||||
|
||||
"untitled".into()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -521,12 +521,27 @@ impl TestAppContext {
|
||||
.downcast_mut::<platform::test::Window>()
|
||||
.unwrap();
|
||||
let mut done_tx = test_window
|
||||
.last_prompt
|
||||
.take()
|
||||
.pending_prompts
|
||||
.borrow_mut()
|
||||
.pop_front()
|
||||
.expect("prompt was not called");
|
||||
let _ = done_tx.try_send(answer);
|
||||
}
|
||||
|
||||
pub fn has_pending_prompt(&self, window_id: usize) -> bool {
|
||||
let mut state = self.cx.borrow_mut();
|
||||
let (_, window) = state
|
||||
.presenters_and_platform_windows
|
||||
.get_mut(&window_id)
|
||||
.unwrap();
|
||||
let test_window = window
|
||||
.as_any_mut()
|
||||
.downcast_mut::<platform::test::Window>()
|
||||
.unwrap();
|
||||
let prompts = test_window.pending_prompts.borrow_mut();
|
||||
!prompts.is_empty()
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn leak_detector(&self) -> Arc<Mutex<LeakDetector>> {
|
||||
self.cx.borrow().leak_detector()
|
||||
|
@ -4,11 +4,12 @@ use crate::{
|
||||
keymap, Action, ClipboardItem,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use collections::VecDeque;
|
||||
use parking_lot::Mutex;
|
||||
use postage::oneshot;
|
||||
use std::{
|
||||
any::Any,
|
||||
cell::{Cell, RefCell},
|
||||
cell::RefCell,
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
@ -36,7 +37,7 @@ pub struct Window {
|
||||
event_handlers: Vec<Box<dyn FnMut(super::Event)>>,
|
||||
resize_handlers: Vec<Box<dyn FnMut()>>,
|
||||
close_handlers: Vec<Box<dyn FnOnce()>>,
|
||||
pub(crate) last_prompt: Cell<Option<oneshot::Sender<usize>>>,
|
||||
pub(crate) pending_prompts: RefCell<VecDeque<oneshot::Sender<usize>>>,
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
@ -188,7 +189,7 @@ impl Window {
|
||||
close_handlers: Vec::new(),
|
||||
scale_factor: 1.0,
|
||||
current_scene: None,
|
||||
last_prompt: Default::default(),
|
||||
pending_prompts: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -242,7 +243,7 @@ impl super::Window for Window {
|
||||
|
||||
fn prompt(&self, _: crate::PromptLevel, _: &str, _: &[&str]) -> oneshot::Receiver<usize> {
|
||||
let (done_tx, done_rx) = oneshot::channel();
|
||||
self.last_prompt.replace(Some(done_tx));
|
||||
self.pending_prompts.borrow_mut().push_back(done_tx);
|
||||
done_rx
|
||||
}
|
||||
|
||||
|
@ -21,6 +21,7 @@ anyhow = "1.0"
|
||||
log = { version = "0.4.16", features = ["kv_unstable_serde"] }
|
||||
postage = { version = "0.4.1", features = ["futures-traits"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
smallvec = { version = "1.6", features = ["union"] }
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { path = "../editor", features = ["test-support"] }
|
||||
|
@ -11,6 +11,7 @@ use gpui::{
|
||||
};
|
||||
use project::{search::SearchQuery, Project};
|
||||
use settings::Settings;
|
||||
use smallvec::SmallVec;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
ops::Range,
|
||||
@ -18,7 +19,8 @@ use std::{
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{
|
||||
menu::Confirm, Item, ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, Workspace,
|
||||
menu::Confirm, Item, ItemHandle, ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView,
|
||||
Workspace,
|
||||
};
|
||||
|
||||
actions!(project_search, [Deploy, SearchInNew, ToggleFocus]);
|
||||
@ -234,8 +236,12 @@ impl Item for ProjectSearchView {
|
||||
None
|
||||
}
|
||||
|
||||
fn project_entry_id(&self, _: &AppContext) -> Option<project::ProjectEntryId> {
|
||||
None
|
||||
fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[project::ProjectEntryId; 3]> {
|
||||
self.results_editor.project_entry_ids(cx)
|
||||
}
|
||||
|
||||
fn is_singleton(&self, _: &AppContext) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn can_save(&self, _: &gpui::AppContext) -> bool {
|
||||
@ -259,10 +265,6 @@ impl Item for ProjectSearchView {
|
||||
.update(cx, |editor, cx| editor.save(project, cx))
|
||||
}
|
||||
|
||||
fn can_save_as(&self, _: &gpui::AppContext) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn save_as(
|
||||
&mut self,
|
||||
_: ModelHandle<Project>,
|
||||
|
@ -136,6 +136,14 @@ impl Settings {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn test_async(cx: &mut gpui::TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings = Self::test(cx);
|
||||
cx.set_global(settings.clone());
|
||||
});
|
||||
}
|
||||
|
||||
pub fn merge(
|
||||
&mut self,
|
||||
data: &SettingsFileContent,
|
||||
|
@ -1,7 +1,7 @@
|
||||
use super::{ItemHandle, SplitDirection};
|
||||
use crate::{toolbar::Toolbar, Item, WeakItemHandle, Workspace};
|
||||
use anyhow::Result;
|
||||
use collections::{HashMap, VecDeque};
|
||||
use collections::{HashMap, HashSet, VecDeque};
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
actions,
|
||||
@ -9,10 +9,10 @@ use gpui::{
|
||||
geometry::{rect::RectF, vector::vec2f},
|
||||
impl_actions, impl_internal_actions,
|
||||
platform::{CursorStyle, NavigationDirection},
|
||||
AppContext, Entity, MutableAppContext, PromptLevel, Quad, RenderContext, Task, View,
|
||||
ViewContext, ViewHandle, WeakViewHandle,
|
||||
AppContext, AsyncAppContext, Entity, ModelHandle, MutableAppContext, PromptLevel, Quad,
|
||||
RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use project::{ProjectEntryId, ProjectPath};
|
||||
use project::{Project, ProjectEntryId, ProjectPath};
|
||||
use serde::Deserialize;
|
||||
use settings::Settings;
|
||||
use std::{any::Any, cell::RefCell, cmp, mem, path::Path, rc::Rc};
|
||||
@ -71,7 +71,11 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_async_action(Pane::close_inactive_items);
|
||||
cx.add_async_action(|workspace: &mut Workspace, action: &CloseItem, cx| {
|
||||
let pane = action.pane.upgrade(cx)?;
|
||||
Some(Pane::close_item(workspace, pane, action.item_id, cx))
|
||||
let task = Pane::close_item(workspace, pane, action.item_id, cx);
|
||||
Some(cx.foreground().spawn(async move {
|
||||
task.await?;
|
||||
Ok(())
|
||||
}))
|
||||
});
|
||||
cx.add_action(|pane: &mut Pane, action: &Split, cx| {
|
||||
pane.split(action.0, cx);
|
||||
@ -294,7 +298,7 @@ impl Pane {
|
||||
) -> Box<dyn ItemHandle> {
|
||||
let existing_item = pane.update(cx, |pane, cx| {
|
||||
for (ix, item) in pane.items.iter().enumerate() {
|
||||
if item.project_entry_id(cx) == Some(project_entry_id) {
|
||||
if item.project_entry_ids(cx).as_slice() == &[project_entry_id] {
|
||||
let item = item.boxed_clone();
|
||||
pane.activate_item(ix, true, focus_item, cx);
|
||||
return Some(item);
|
||||
@ -351,27 +355,13 @@ impl Pane {
|
||||
self.items.get(self.active_item_index).cloned()
|
||||
}
|
||||
|
||||
pub fn project_entry_id_for_item(
|
||||
&self,
|
||||
item: &dyn ItemHandle,
|
||||
cx: &AppContext,
|
||||
) -> Option<ProjectEntryId> {
|
||||
self.items.iter().find_map(|existing| {
|
||||
if existing.id() == item.id() {
|
||||
existing.project_entry_id(cx)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn item_for_entry(
|
||||
&self,
|
||||
entry_id: ProjectEntryId,
|
||||
cx: &AppContext,
|
||||
) -> Option<Box<dyn ItemHandle>> {
|
||||
self.items.iter().find_map(|item| {
|
||||
if item.project_entry_id(cx) == Some(entry_id) {
|
||||
if item.is_singleton(cx) && item.project_entry_ids(cx).as_slice() == &[entry_id] {
|
||||
Some(item.boxed_clone())
|
||||
} else {
|
||||
None
|
||||
@ -445,12 +435,13 @@ impl Pane {
|
||||
None
|
||||
} else {
|
||||
let item_id_to_close = pane.items[pane.active_item_index].id();
|
||||
Some(Self::close_items(
|
||||
workspace,
|
||||
pane_handle,
|
||||
cx,
|
||||
move |item_id| item_id == item_id_to_close,
|
||||
))
|
||||
let task = Self::close_items(workspace, pane_handle, cx, move |item_id| {
|
||||
item_id == item_id_to_close
|
||||
});
|
||||
Some(cx.foreground().spawn(async move {
|
||||
task.await?;
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@ -465,8 +456,11 @@ impl Pane {
|
||||
None
|
||||
} else {
|
||||
let active_item_id = pane.items[pane.active_item_index].id();
|
||||
Some(Self::close_items(workspace, pane_handle, cx, move |id| {
|
||||
id != active_item_id
|
||||
let task =
|
||||
Self::close_items(workspace, pane_handle, cx, move |id| id != active_item_id);
|
||||
Some(cx.foreground().spawn(async move {
|
||||
task.await?;
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
}
|
||||
@ -476,7 +470,7 @@ impl Pane {
|
||||
pane: ViewHandle<Pane>,
|
||||
item_id_to_close: usize,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) -> Task<Result<()>> {
|
||||
) -> Task<Result<bool>> {
|
||||
Self::close_items(workspace, pane, cx, move |view_id| {
|
||||
view_id == item_id_to_close
|
||||
})
|
||||
@ -487,108 +481,72 @@ impl Pane {
|
||||
pane: ViewHandle<Pane>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
should_close: impl 'static + Fn(usize) -> bool,
|
||||
) -> Task<Result<()>> {
|
||||
const CONFLICT_MESSAGE: &'static str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
|
||||
const DIRTY_MESSAGE: &'static str =
|
||||
"This file contains unsaved edits. Do you want to save it?";
|
||||
|
||||
) -> Task<Result<bool>> {
|
||||
let project = workspace.project().clone();
|
||||
|
||||
// Find the items to close.
|
||||
let mut items_to_close = Vec::new();
|
||||
for item in &pane.read(cx).items {
|
||||
if should_close(item.id()) {
|
||||
items_to_close.push(item.boxed_clone());
|
||||
}
|
||||
}
|
||||
|
||||
// If a buffer is open both in a singleton editor and in a multibuffer, make sure
|
||||
// to focus the singleton buffer when prompting to save that buffer, as opposed
|
||||
// to focusing the multibuffer, because this gives the user a more clear idea
|
||||
// of what content they would be saving.
|
||||
items_to_close.sort_by_key(|item| !item.is_singleton(cx));
|
||||
|
||||
cx.spawn(|workspace, mut cx| async move {
|
||||
while let Some(item_to_close_ix) = pane.read_with(&cx, |pane, _| {
|
||||
pane.items.iter().position(|item| should_close(item.id()))
|
||||
}) {
|
||||
let item =
|
||||
pane.read_with(&cx, |pane, _| pane.items[item_to_close_ix].boxed_clone());
|
||||
|
||||
let is_last_item_for_entry = workspace.read_with(&cx, |workspace, cx| {
|
||||
let project_entry_id = item.project_entry_id(cx);
|
||||
project_entry_id.is_none()
|
||||
|| workspace
|
||||
.items(cx)
|
||||
.filter(|item| item.project_entry_id(cx) == project_entry_id)
|
||||
.count()
|
||||
== 1
|
||||
let mut saved_project_entry_ids = HashSet::default();
|
||||
for item in items_to_close.clone() {
|
||||
// Find the item's current index and its set of project entries. Avoid
|
||||
// storing these in advance, in case they have changed since this task
|
||||
// was started.
|
||||
let (item_ix, mut project_entry_ids) = pane.read_with(&cx, |pane, cx| {
|
||||
(pane.index_for_item(&*item), item.project_entry_ids(cx))
|
||||
});
|
||||
let item_ix = if let Some(ix) = item_ix {
|
||||
ix
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if is_last_item_for_entry {
|
||||
if cx.read(|cx| item.has_conflict(cx) && item.can_save(cx)) {
|
||||
let mut answer = pane.update(&mut cx, |pane, cx| {
|
||||
pane.activate_item(item_to_close_ix, true, true, cx);
|
||||
cx.prompt(
|
||||
PromptLevel::Warning,
|
||||
CONFLICT_MESSAGE,
|
||||
&["Overwrite", "Discard", "Cancel"],
|
||||
)
|
||||
});
|
||||
|
||||
match answer.next().await {
|
||||
Some(0) => {
|
||||
cx.update(|cx| item.save(project.clone(), cx)).await?;
|
||||
}
|
||||
Some(1) => {
|
||||
cx.update(|cx| item.reload(project.clone(), cx)).await?;
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
} else if cx.read(|cx| item.is_dirty(cx)) {
|
||||
if cx.read(|cx| item.can_save(cx)) {
|
||||
let mut answer = pane.update(&mut cx, |pane, cx| {
|
||||
pane.activate_item(item_to_close_ix, true, true, cx);
|
||||
cx.prompt(
|
||||
PromptLevel::Warning,
|
||||
DIRTY_MESSAGE,
|
||||
&["Save", "Don't Save", "Cancel"],
|
||||
)
|
||||
});
|
||||
|
||||
match answer.next().await {
|
||||
Some(0) => {
|
||||
cx.update(|cx| item.save(project.clone(), cx)).await?;
|
||||
}
|
||||
Some(1) => {}
|
||||
_ => break,
|
||||
}
|
||||
} else if cx.read(|cx| item.can_save_as(cx)) {
|
||||
let mut answer = pane.update(&mut cx, |pane, cx| {
|
||||
pane.activate_item(item_to_close_ix, true, true, cx);
|
||||
cx.prompt(
|
||||
PromptLevel::Warning,
|
||||
DIRTY_MESSAGE,
|
||||
&["Save", "Don't Save", "Cancel"],
|
||||
)
|
||||
});
|
||||
|
||||
match answer.next().await {
|
||||
Some(0) => {
|
||||
let start_abs_path = project
|
||||
.read_with(&cx, |project, cx| {
|
||||
let worktree = project.visible_worktrees(cx).next()?;
|
||||
Some(
|
||||
worktree
|
||||
.read(cx)
|
||||
.as_local()?
|
||||
.abs_path()
|
||||
.to_path_buf(),
|
||||
)
|
||||
})
|
||||
.unwrap_or(Path::new("").into());
|
||||
|
||||
let mut abs_path =
|
||||
cx.update(|cx| cx.prompt_for_new_path(&start_abs_path));
|
||||
if let Some(abs_path) = abs_path.next().await.flatten() {
|
||||
cx.update(|cx| item.save_as(project.clone(), abs_path, cx))
|
||||
.await?;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Some(1) => {}
|
||||
_ => break,
|
||||
// If an item hasn't yet been associated with a project entry, then always
|
||||
// prompt to save it before closing it. Otherwise, check if the item has
|
||||
// any project entries that are not open anywhere else in the workspace,
|
||||
// AND that the user has not already been prompted to save. If there are
|
||||
// any such project entries, prompt the user to save this item.
|
||||
let should_save = if project_entry_ids.is_empty() {
|
||||
true
|
||||
} else {
|
||||
workspace.read_with(&cx, |workspace, cx| {
|
||||
for item in workspace.items(cx) {
|
||||
if !items_to_close
|
||||
.iter()
|
||||
.any(|item_to_close| item_to_close.id() == item.id())
|
||||
{
|
||||
let other_project_entry_ids = item.project_entry_ids(cx);
|
||||
project_entry_ids
|
||||
.retain(|id| !other_project_entry_ids.contains(&id));
|
||||
}
|
||||
}
|
||||
});
|
||||
project_entry_ids
|
||||
.iter()
|
||||
.any(|id| saved_project_entry_ids.insert(*id))
|
||||
};
|
||||
|
||||
if should_save {
|
||||
if !Self::save_item(project.clone(), &pane, item_ix, &item, true, &mut cx)
|
||||
.await?
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the item from the pane.
|
||||
pane.update(&mut cx, |pane, cx| {
|
||||
if let Some(item_ix) = pane.items.iter().position(|i| i.id() == item.id()) {
|
||||
if item_ix == pane.active_item_index {
|
||||
@ -621,10 +579,88 @@ impl Pane {
|
||||
}
|
||||
|
||||
pane.update(&mut cx, |_, cx| cx.notify());
|
||||
Ok(())
|
||||
Ok(true)
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn save_item(
|
||||
project: ModelHandle<Project>,
|
||||
pane: &ViewHandle<Pane>,
|
||||
item_ix: usize,
|
||||
item: &Box<dyn ItemHandle>,
|
||||
should_prompt_for_save: bool,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<bool> {
|
||||
const CONFLICT_MESSAGE: &'static str =
|
||||
"This file has changed on disk since you started editing it. Do you want to overwrite it?";
|
||||
const DIRTY_MESSAGE: &'static str =
|
||||
"This file contains unsaved edits. Do you want to save it?";
|
||||
|
||||
let (has_conflict, is_dirty, can_save, is_singleton) = cx.read(|cx| {
|
||||
(
|
||||
item.has_conflict(cx),
|
||||
item.is_dirty(cx),
|
||||
item.can_save(cx),
|
||||
item.is_singleton(cx),
|
||||
)
|
||||
});
|
||||
|
||||
if has_conflict && can_save {
|
||||
let mut answer = pane.update(cx, |pane, cx| {
|
||||
pane.activate_item(item_ix, true, true, cx);
|
||||
cx.prompt(
|
||||
PromptLevel::Warning,
|
||||
CONFLICT_MESSAGE,
|
||||
&["Overwrite", "Discard", "Cancel"],
|
||||
)
|
||||
});
|
||||
match answer.next().await {
|
||||
Some(0) => cx.update(|cx| item.save(project, cx)).await?,
|
||||
Some(1) => cx.update(|cx| item.reload(project, cx)).await?,
|
||||
_ => return Ok(false),
|
||||
}
|
||||
} else if is_dirty && (can_save || is_singleton) {
|
||||
let should_save = if should_prompt_for_save {
|
||||
let mut answer = pane.update(cx, |pane, cx| {
|
||||
pane.activate_item(item_ix, true, true, cx);
|
||||
cx.prompt(
|
||||
PromptLevel::Warning,
|
||||
DIRTY_MESSAGE,
|
||||
&["Save", "Don't Save", "Cancel"],
|
||||
)
|
||||
});
|
||||
match answer.next().await {
|
||||
Some(0) => true,
|
||||
Some(1) => false,
|
||||
_ => return Ok(false),
|
||||
}
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
if should_save {
|
||||
if can_save {
|
||||
cx.update(|cx| item.save(project, cx)).await?;
|
||||
} else if is_singleton {
|
||||
let start_abs_path = project
|
||||
.read_with(cx, |project, cx| {
|
||||
let worktree = project.visible_worktrees(cx).next()?;
|
||||
Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
|
||||
})
|
||||
.unwrap_or(Path::new("").into());
|
||||
|
||||
let mut abs_path = cx.update(|cx| cx.prompt_for_new_path(&start_abs_path));
|
||||
if let Some(abs_path) = abs_path.next().await.flatten() {
|
||||
cx.update(|cx| item.save_as(project, abs_path, cx)).await?;
|
||||
} else {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if let Some(active_item) = self.active_item() {
|
||||
cx.focus(active_item);
|
||||
@ -916,253 +952,3 @@ impl NavHistory {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::AppState;
|
||||
use gpui::{ModelHandle, TestAppContext, ViewContext};
|
||||
use project::Project;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_close_items(cx: &mut TestAppContext) {
|
||||
cx.foreground().forbid_parking();
|
||||
|
||||
let app_state = cx.update(AppState::test);
|
||||
let project = Project::test(app_state.fs.clone(), None, cx).await;
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
|
||||
let item1 = cx.add_view(window_id, |_| {
|
||||
let mut item = TestItem::new();
|
||||
item.is_dirty = true;
|
||||
item
|
||||
});
|
||||
let item2 = cx.add_view(window_id, |_| {
|
||||
let mut item = TestItem::new();
|
||||
item.is_dirty = true;
|
||||
item.has_conflict = true;
|
||||
item
|
||||
});
|
||||
let item3 = cx.add_view(window_id, |_| {
|
||||
let mut item = TestItem::new();
|
||||
item.is_dirty = true;
|
||||
item.has_conflict = true;
|
||||
item
|
||||
});
|
||||
let item4 = cx.add_view(window_id, |_| {
|
||||
let mut item = TestItem::new();
|
||||
item.is_dirty = true;
|
||||
item.can_save = false;
|
||||
item
|
||||
});
|
||||
let pane = workspace.update(cx, |workspace, cx| {
|
||||
workspace.add_item(Box::new(item1.clone()), cx);
|
||||
workspace.add_item(Box::new(item2.clone()), cx);
|
||||
workspace.add_item(Box::new(item3.clone()), cx);
|
||||
workspace.add_item(Box::new(item4.clone()), cx);
|
||||
workspace.active_pane().clone()
|
||||
});
|
||||
|
||||
let close_items = workspace.update(cx, |workspace, cx| {
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.activate_item(1, true, true, cx);
|
||||
assert_eq!(pane.active_item().unwrap().id(), item2.id());
|
||||
});
|
||||
|
||||
let item1_id = item1.id();
|
||||
let item3_id = item3.id();
|
||||
let item4_id = item4.id();
|
||||
Pane::close_items(workspace, pane.clone(), cx, move |id| {
|
||||
[item1_id, item3_id, item4_id].contains(&id)
|
||||
})
|
||||
});
|
||||
|
||||
cx.foreground().run_until_parked();
|
||||
pane.read_with(cx, |pane, _| {
|
||||
assert_eq!(pane.items.len(), 4);
|
||||
assert_eq!(pane.active_item().unwrap().id(), item1.id());
|
||||
});
|
||||
|
||||
cx.simulate_prompt_answer(window_id, 0);
|
||||
cx.foreground().run_until_parked();
|
||||
pane.read_with(cx, |pane, cx| {
|
||||
assert_eq!(item1.read(cx).save_count, 1);
|
||||
assert_eq!(item1.read(cx).save_as_count, 0);
|
||||
assert_eq!(item1.read(cx).reload_count, 0);
|
||||
assert_eq!(pane.items.len(), 3);
|
||||
assert_eq!(pane.active_item().unwrap().id(), item3.id());
|
||||
});
|
||||
|
||||
cx.simulate_prompt_answer(window_id, 1);
|
||||
cx.foreground().run_until_parked();
|
||||
pane.read_with(cx, |pane, cx| {
|
||||
assert_eq!(item3.read(cx).save_count, 0);
|
||||
assert_eq!(item3.read(cx).save_as_count, 0);
|
||||
assert_eq!(item3.read(cx).reload_count, 1);
|
||||
assert_eq!(pane.items.len(), 2);
|
||||
assert_eq!(pane.active_item().unwrap().id(), item4.id());
|
||||
});
|
||||
|
||||
cx.simulate_prompt_answer(window_id, 0);
|
||||
cx.foreground().run_until_parked();
|
||||
cx.simulate_new_path_selection(|_| Some(Default::default()));
|
||||
close_items.await.unwrap();
|
||||
pane.read_with(cx, |pane, cx| {
|
||||
assert_eq!(item4.read(cx).save_count, 0);
|
||||
assert_eq!(item4.read(cx).save_as_count, 1);
|
||||
assert_eq!(item4.read(cx).reload_count, 0);
|
||||
assert_eq!(pane.items.len(), 1);
|
||||
assert_eq!(pane.active_item().unwrap().id(), item2.id());
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_prompting_only_on_last_item_for_entry(cx: &mut TestAppContext) {
|
||||
cx.foreground().forbid_parking();
|
||||
|
||||
let app_state = cx.update(AppState::test);
|
||||
let project = Project::test(app_state.fs.clone(), [], cx).await;
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
|
||||
let item = cx.add_view(window_id, |_| {
|
||||
let mut item = TestItem::new();
|
||||
item.is_dirty = true;
|
||||
item.project_entry_id = Some(ProjectEntryId::new(&AtomicUsize::new(1)));
|
||||
item
|
||||
});
|
||||
|
||||
let (left_pane, right_pane) = workspace.update(cx, |workspace, cx| {
|
||||
workspace.add_item(Box::new(item.clone()), cx);
|
||||
let left_pane = workspace.active_pane().clone();
|
||||
let right_pane = workspace.split_pane(left_pane.clone(), SplitDirection::Right, cx);
|
||||
(left_pane, right_pane)
|
||||
});
|
||||
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
let item = right_pane.read(cx).active_item().unwrap();
|
||||
Pane::close_item(workspace, right_pane.clone(), item.id(), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
workspace.read_with(cx, |workspace, _| {
|
||||
assert_eq!(workspace.panes(), [left_pane.clone()]);
|
||||
});
|
||||
|
||||
let close_item = workspace.update(cx, |workspace, cx| {
|
||||
let item = left_pane.read(cx).active_item().unwrap();
|
||||
Pane::close_item(workspace, left_pane.clone(), item.id(), cx)
|
||||
});
|
||||
cx.foreground().run_until_parked();
|
||||
cx.simulate_prompt_answer(window_id, 0);
|
||||
close_item.await.unwrap();
|
||||
left_pane.read_with(cx, |pane, _| {
|
||||
assert_eq!(pane.items.len(), 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TestItem {
|
||||
save_count: usize,
|
||||
save_as_count: usize,
|
||||
reload_count: usize,
|
||||
is_dirty: bool,
|
||||
has_conflict: bool,
|
||||
can_save: bool,
|
||||
project_entry_id: Option<ProjectEntryId>,
|
||||
}
|
||||
|
||||
impl TestItem {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
save_count: 0,
|
||||
save_as_count: 0,
|
||||
reload_count: 0,
|
||||
is_dirty: false,
|
||||
has_conflict: false,
|
||||
can_save: true,
|
||||
project_entry_id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for TestItem {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for TestItem {
|
||||
fn ui_name() -> &'static str {
|
||||
"TestItem"
|
||||
}
|
||||
|
||||
fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
|
||||
Empty::new().boxed()
|
||||
}
|
||||
}
|
||||
|
||||
impl Item for TestItem {
|
||||
fn tab_content(&self, _: &theme::Tab, _: &AppContext) -> ElementBox {
|
||||
Empty::new().boxed()
|
||||
}
|
||||
|
||||
fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
|
||||
None
|
||||
}
|
||||
|
||||
fn project_entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
|
||||
self.project_entry_id
|
||||
}
|
||||
|
||||
fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext<Self>) {}
|
||||
|
||||
fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Some(self.clone())
|
||||
}
|
||||
|
||||
fn is_dirty(&self, _: &AppContext) -> bool {
|
||||
self.is_dirty
|
||||
}
|
||||
|
||||
fn has_conflict(&self, _: &AppContext) -> bool {
|
||||
self.has_conflict
|
||||
}
|
||||
|
||||
fn can_save(&self, _: &AppContext) -> bool {
|
||||
self.can_save
|
||||
}
|
||||
|
||||
fn save(
|
||||
&mut self,
|
||||
_: ModelHandle<Project>,
|
||||
_: &mut ViewContext<Self>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
self.save_count += 1;
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn can_save_as(&self, _: &AppContext) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn save_as(
|
||||
&mut self,
|
||||
_: ModelHandle<Project>,
|
||||
_: std::path::PathBuf,
|
||||
_: &mut ViewContext<Self>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
self.save_as_count += 1;
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn reload(
|
||||
&mut self,
|
||||
_: ModelHandle<Project>,
|
||||
_: &mut ViewContext<Self>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
self.reload_count += 1;
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -33,6 +33,7 @@ use postage::prelude::Stream;
|
||||
use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, Worktree};
|
||||
use settings::Settings;
|
||||
use sidebar::{Side, Sidebar, SidebarButtons, ToggleSidebarItem, ToggleSidebarItemFocus};
|
||||
use smallvec::SmallVec;
|
||||
use status_bar::StatusBar;
|
||||
pub use status_bar::StatusItemView;
|
||||
use std::{
|
||||
@ -75,9 +76,14 @@ actions!(
|
||||
workspace,
|
||||
[
|
||||
Open,
|
||||
OpenNew,
|
||||
NewFile,
|
||||
NewWindow,
|
||||
CloseWindow,
|
||||
AddFolderToProject,
|
||||
Unfollow,
|
||||
Save,
|
||||
SaveAs,
|
||||
SaveAll,
|
||||
ActivatePreviousPane,
|
||||
ActivateNextPane,
|
||||
FollowNextCollaborator,
|
||||
@ -114,7 +120,15 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||
});
|
||||
cx.add_global_action({
|
||||
let app_state = Arc::downgrade(&app_state);
|
||||
move |_: &OpenNew, cx: &mut MutableAppContext| {
|
||||
move |_: &NewFile, cx: &mut MutableAppContext| {
|
||||
if let Some(app_state) = app_state.upgrade() {
|
||||
open_new(&app_state, cx)
|
||||
}
|
||||
}
|
||||
});
|
||||
cx.add_global_action({
|
||||
let app_state = Arc::downgrade(&app_state);
|
||||
move |_: &NewWindow, cx: &mut MutableAppContext| {
|
||||
if let Some(app_state) = app_state.upgrade() {
|
||||
open_new(&app_state, cx)
|
||||
}
|
||||
@ -131,6 +145,9 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||
|
||||
cx.add_async_action(Workspace::toggle_follow);
|
||||
cx.add_async_action(Workspace::follow_next_collaborator);
|
||||
cx.add_async_action(Workspace::close);
|
||||
cx.add_async_action(Workspace::save_all);
|
||||
cx.add_action(Workspace::add_folder_to_project);
|
||||
cx.add_action(
|
||||
|workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext<Workspace>| {
|
||||
let pane = workspace.active_pane().clone();
|
||||
@ -139,7 +156,12 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
|
||||
);
|
||||
cx.add_action(
|
||||
|workspace: &mut Workspace, _: &Save, cx: &mut ViewContext<Workspace>| {
|
||||
workspace.save_active_item(cx).detach_and_log_err(cx);
|
||||
workspace.save_active_item(false, cx).detach_and_log_err(cx);
|
||||
},
|
||||
);
|
||||
cx.add_action(
|
||||
|workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext<Workspace>| {
|
||||
workspace.save_active_item(true, cx).detach_and_log_err(cx);
|
||||
},
|
||||
);
|
||||
cx.add_action(Workspace::toggle_sidebar_item);
|
||||
@ -200,7 +222,8 @@ pub trait Item: View {
|
||||
}
|
||||
fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox;
|
||||
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
|
||||
fn project_entry_id(&self, cx: &AppContext) -> Option<ProjectEntryId>;
|
||||
fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
|
||||
fn is_singleton(&self, cx: &AppContext) -> bool;
|
||||
fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext<Self>);
|
||||
fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
|
||||
where
|
||||
@ -220,7 +243,6 @@ pub trait Item: View {
|
||||
project: ModelHandle<Project>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>>;
|
||||
fn can_save_as(&self, cx: &AppContext) -> bool;
|
||||
fn save_as(
|
||||
&mut self,
|
||||
project: ModelHandle<Project>,
|
||||
@ -350,7 +372,8 @@ impl<T: FollowableItem> FollowableItemHandle for ViewHandle<T> {
|
||||
pub trait ItemHandle: 'static + fmt::Debug {
|
||||
fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox;
|
||||
fn project_path(&self, cx: &AppContext) -> Option<ProjectPath>;
|
||||
fn project_entry_id(&self, cx: &AppContext) -> Option<ProjectEntryId>;
|
||||
fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
|
||||
fn is_singleton(&self, cx: &AppContext) -> bool;
|
||||
fn boxed_clone(&self) -> Box<dyn ItemHandle>;
|
||||
fn set_nav_history(&self, nav_history: Rc<RefCell<NavHistory>>, cx: &mut MutableAppContext);
|
||||
fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemHandle>>;
|
||||
@ -367,7 +390,6 @@ pub trait ItemHandle: 'static + fmt::Debug {
|
||||
fn is_dirty(&self, cx: &AppContext) -> bool;
|
||||
fn has_conflict(&self, cx: &AppContext) -> bool;
|
||||
fn can_save(&self, cx: &AppContext) -> bool;
|
||||
fn can_save_as(&self, cx: &AppContext) -> bool;
|
||||
fn save(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext) -> Task<Result<()>>;
|
||||
fn save_as(
|
||||
&self,
|
||||
@ -411,8 +433,12 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
|
||||
self.read(cx).project_path(cx)
|
||||
}
|
||||
|
||||
fn project_entry_id(&self, cx: &AppContext) -> Option<ProjectEntryId> {
|
||||
self.read(cx).project_entry_id(cx)
|
||||
fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]> {
|
||||
self.read(cx).project_entry_ids(cx)
|
||||
}
|
||||
|
||||
fn is_singleton(&self, cx: &AppContext) -> bool {
|
||||
self.read(cx).is_singleton(cx)
|
||||
}
|
||||
|
||||
fn boxed_clone(&self) -> Box<dyn ItemHandle> {
|
||||
@ -540,10 +566,6 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
|
||||
self.read(cx).can_save(cx)
|
||||
}
|
||||
|
||||
fn can_save_as(&self, cx: &AppContext) -> bool {
|
||||
self.read(cx).can_save_as(cx)
|
||||
}
|
||||
|
||||
fn save(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext) -> Task<Result<()>> {
|
||||
self.update(cx, |item, cx| item.save(project, cx))
|
||||
}
|
||||
@ -864,6 +886,79 @@ impl Workspace {
|
||||
}
|
||||
}
|
||||
|
||||
fn close(&mut self, _: &CloseWindow, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
|
||||
let prepare = self.prepare_to_close(cx);
|
||||
Some(cx.spawn(|this, mut cx| async move {
|
||||
if prepare.await? {
|
||||
this.update(&mut cx, |_, cx| {
|
||||
let window_id = cx.window_id();
|
||||
cx.remove_window(window_id);
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
|
||||
fn prepare_to_close(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<bool>> {
|
||||
self.save_all_internal(true, cx)
|
||||
}
|
||||
|
||||
fn save_all(&mut self, _: &SaveAll, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
|
||||
let save_all = self.save_all_internal(false, cx);
|
||||
Some(cx.foreground().spawn(async move {
|
||||
save_all.await?;
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
|
||||
fn save_all_internal(
|
||||
&mut self,
|
||||
should_prompt_to_save: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<bool>> {
|
||||
let dirty_items = self
|
||||
.panes
|
||||
.iter()
|
||||
.flat_map(|pane| {
|
||||
pane.read(cx).items().filter_map(|item| {
|
||||
if item.is_dirty(cx) {
|
||||
Some((pane.clone(), item.boxed_clone()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let project = self.project.clone();
|
||||
cx.spawn_weak(|_, mut cx| async move {
|
||||
// let mut saved_project_entry_ids = HashSet::default();
|
||||
for (pane, item) in dirty_items {
|
||||
let (is_singl, project_entry_ids) =
|
||||
cx.read(|cx| (item.is_singleton(cx), item.project_entry_ids(cx)));
|
||||
if is_singl || !project_entry_ids.is_empty() {
|
||||
if let Some(ix) =
|
||||
pane.read_with(&cx, |pane, _| pane.index_for_item(item.as_ref()))
|
||||
{
|
||||
if !Pane::save_item(
|
||||
project.clone(),
|
||||
&pane,
|
||||
ix,
|
||||
&item,
|
||||
should_prompt_to_save,
|
||||
&mut cx,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn open_paths(
|
||||
&mut self,
|
||||
mut abs_paths: Vec<PathBuf>,
|
||||
@ -912,6 +1007,27 @@ impl Workspace {
|
||||
})
|
||||
}
|
||||
|
||||
fn add_folder_to_project(&mut self, _: &AddFolderToProject, cx: &mut ViewContext<Self>) {
|
||||
let mut paths = cx.prompt_for_paths(PathPromptOptions {
|
||||
files: false,
|
||||
directories: true,
|
||||
multiple: true,
|
||||
});
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
if let Some(paths) = paths.recv().await.flatten() {
|
||||
let results = this
|
||||
.update(&mut cx, |this, cx| this.open_paths(paths, cx))
|
||||
.await;
|
||||
for result in results {
|
||||
if let Some(result) = result {
|
||||
result.log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn project_path_for_path(
|
||||
&self,
|
||||
abs_path: &Path,
|
||||
@ -1032,10 +1148,14 @@ impl Workspace {
|
||||
self.active_item(cx).and_then(|item| item.project_path(cx))
|
||||
}
|
||||
|
||||
pub fn save_active_item(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
|
||||
pub fn save_active_item(
|
||||
&mut self,
|
||||
force_name_change: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let project = self.project.clone();
|
||||
if let Some(item) = self.active_item(cx) {
|
||||
if item.can_save(cx) {
|
||||
if !force_name_change && item.can_save(cx) {
|
||||
if item.has_conflict(cx.as_ref()) {
|
||||
const CONFLICT_MESSAGE: &'static str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
|
||||
|
||||
@ -1054,7 +1174,7 @@ impl Workspace {
|
||||
} else {
|
||||
item.save(project, cx)
|
||||
}
|
||||
} else if item.can_save_as(cx) {
|
||||
} else if item.is_singleton(cx) {
|
||||
let worktree = self.worktrees(cx).next();
|
||||
let start_abs_path = worktree
|
||||
.and_then(|w| w.read(cx).as_local())
|
||||
@ -2287,5 +2407,361 @@ fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
|
||||
(app_state.initialize_workspace)(&mut workspace, app_state, cx);
|
||||
workspace
|
||||
});
|
||||
cx.dispatch_action(window_id, vec![workspace.id()], &OpenNew);
|
||||
cx.dispatch_action(window_id, vec![workspace.id()], &NewFile);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::{ModelHandle, TestAppContext, ViewContext};
|
||||
use project::{FakeFs, Project, ProjectEntryId};
|
||||
use serde_json::json;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_close_window(cx: &mut TestAppContext) {
|
||||
cx.foreground().forbid_parking();
|
||||
Settings::test_async(cx);
|
||||
let fs = FakeFs::new(cx.background());
|
||||
fs.insert_tree("/root", json!({ "one": "" })).await;
|
||||
|
||||
let project = Project::test(fs, ["root".as_ref()], cx).await;
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project.clone(), cx));
|
||||
|
||||
// When there are no dirty items, there's nothing to do.
|
||||
let item1 = cx.add_view(window_id, |_| TestItem::new());
|
||||
workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx));
|
||||
let task = workspace.update(cx, |w, cx| w.prepare_to_close(cx));
|
||||
assert_eq!(task.await.unwrap(), true);
|
||||
|
||||
// When there are dirty untitled items, prompt to save each one. If the user
|
||||
// cancels any prompt, then abort.
|
||||
let item2 = cx.add_view(window_id, |_| {
|
||||
let mut item = TestItem::new();
|
||||
item.is_dirty = true;
|
||||
item
|
||||
});
|
||||
let item3 = cx.add_view(window_id, |_| {
|
||||
let mut item = TestItem::new();
|
||||
item.is_dirty = true;
|
||||
item.project_entry_ids = vec![ProjectEntryId::from_proto(1)];
|
||||
item
|
||||
});
|
||||
workspace.update(cx, |w, cx| {
|
||||
w.add_item(Box::new(item2.clone()), cx);
|
||||
w.add_item(Box::new(item3.clone()), cx);
|
||||
});
|
||||
let task = workspace.update(cx, |w, cx| w.prepare_to_close(cx));
|
||||
cx.foreground().run_until_parked();
|
||||
cx.simulate_prompt_answer(window_id, 2 /* cancel */);
|
||||
cx.foreground().run_until_parked();
|
||||
assert!(!cx.has_pending_prompt(window_id));
|
||||
assert_eq!(task.await.unwrap(), false);
|
||||
|
||||
// If there are multiple dirty items representing the same project entry.
|
||||
workspace.update(cx, |w, cx| {
|
||||
w.add_item(Box::new(item2.clone()), cx);
|
||||
w.add_item(Box::new(item3.clone()), cx);
|
||||
});
|
||||
let task = workspace.update(cx, |w, cx| w.prepare_to_close(cx));
|
||||
cx.foreground().run_until_parked();
|
||||
cx.simulate_prompt_answer(window_id, 2 /* cancel */);
|
||||
cx.foreground().run_until_parked();
|
||||
assert!(!cx.has_pending_prompt(window_id));
|
||||
assert_eq!(task.await.unwrap(), false);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_close_pane_items(cx: &mut TestAppContext) {
|
||||
cx.foreground().forbid_parking();
|
||||
Settings::test_async(cx);
|
||||
let fs = FakeFs::new(cx.background());
|
||||
|
||||
let project = Project::test(fs, None, cx).await;
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
|
||||
|
||||
let item1 = cx.add_view(window_id, |_| {
|
||||
let mut item = TestItem::new();
|
||||
item.is_dirty = true;
|
||||
item.project_entry_ids = vec![ProjectEntryId::from_proto(1)];
|
||||
item
|
||||
});
|
||||
let item2 = cx.add_view(window_id, |_| {
|
||||
let mut item = TestItem::new();
|
||||
item.is_dirty = true;
|
||||
item.has_conflict = true;
|
||||
item.project_entry_ids = vec![ProjectEntryId::from_proto(2)];
|
||||
item
|
||||
});
|
||||
let item3 = cx.add_view(window_id, |_| {
|
||||
let mut item = TestItem::new();
|
||||
item.is_dirty = true;
|
||||
item.has_conflict = true;
|
||||
item.project_entry_ids = vec![ProjectEntryId::from_proto(3)];
|
||||
item
|
||||
});
|
||||
let item4 = cx.add_view(window_id, |_| {
|
||||
let mut item = TestItem::new();
|
||||
item.is_dirty = true;
|
||||
item
|
||||
});
|
||||
let pane = workspace.update(cx, |workspace, cx| {
|
||||
workspace.add_item(Box::new(item1.clone()), cx);
|
||||
workspace.add_item(Box::new(item2.clone()), cx);
|
||||
workspace.add_item(Box::new(item3.clone()), cx);
|
||||
workspace.add_item(Box::new(item4.clone()), cx);
|
||||
workspace.active_pane().clone()
|
||||
});
|
||||
|
||||
let close_items = workspace.update(cx, |workspace, cx| {
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.activate_item(1, true, true, cx);
|
||||
assert_eq!(pane.active_item().unwrap().id(), item2.id());
|
||||
});
|
||||
|
||||
let item1_id = item1.id();
|
||||
let item3_id = item3.id();
|
||||
let item4_id = item4.id();
|
||||
Pane::close_items(workspace, pane.clone(), cx, move |id| {
|
||||
[item1_id, item3_id, item4_id].contains(&id)
|
||||
})
|
||||
});
|
||||
|
||||
cx.foreground().run_until_parked();
|
||||
pane.read_with(cx, |pane, _| {
|
||||
assert_eq!(pane.items().count(), 4);
|
||||
assert_eq!(pane.active_item().unwrap().id(), item1.id());
|
||||
});
|
||||
|
||||
cx.simulate_prompt_answer(window_id, 0);
|
||||
cx.foreground().run_until_parked();
|
||||
pane.read_with(cx, |pane, cx| {
|
||||
assert_eq!(item1.read(cx).save_count, 1);
|
||||
assert_eq!(item1.read(cx).save_as_count, 0);
|
||||
assert_eq!(item1.read(cx).reload_count, 0);
|
||||
assert_eq!(pane.items().count(), 3);
|
||||
assert_eq!(pane.active_item().unwrap().id(), item3.id());
|
||||
});
|
||||
|
||||
cx.simulate_prompt_answer(window_id, 1);
|
||||
cx.foreground().run_until_parked();
|
||||
pane.read_with(cx, |pane, cx| {
|
||||
assert_eq!(item3.read(cx).save_count, 0);
|
||||
assert_eq!(item3.read(cx).save_as_count, 0);
|
||||
assert_eq!(item3.read(cx).reload_count, 1);
|
||||
assert_eq!(pane.items().count(), 2);
|
||||
assert_eq!(pane.active_item().unwrap().id(), item4.id());
|
||||
});
|
||||
|
||||
cx.simulate_prompt_answer(window_id, 0);
|
||||
cx.foreground().run_until_parked();
|
||||
cx.simulate_new_path_selection(|_| Some(Default::default()));
|
||||
close_items.await.unwrap();
|
||||
pane.read_with(cx, |pane, cx| {
|
||||
assert_eq!(item4.read(cx).save_count, 0);
|
||||
assert_eq!(item4.read(cx).save_as_count, 1);
|
||||
assert_eq!(item4.read(cx).reload_count, 0);
|
||||
assert_eq!(pane.items().count(), 1);
|
||||
assert_eq!(pane.active_item().unwrap().id(), item2.id());
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) {
|
||||
cx.foreground().forbid_parking();
|
||||
Settings::test_async(cx);
|
||||
let fs = FakeFs::new(cx.background());
|
||||
|
||||
let project = Project::test(fs, [], cx).await;
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
|
||||
|
||||
// Create several workspace items with single project entries, and two
|
||||
// workspace items with multiple project entries.
|
||||
let single_entry_items = (0..=4)
|
||||
.map(|project_entry_id| {
|
||||
let mut item = TestItem::new();
|
||||
item.is_dirty = true;
|
||||
item.project_entry_ids = vec![ProjectEntryId::from_proto(project_entry_id)];
|
||||
item.is_singleton = true;
|
||||
item
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let item_2_3 = {
|
||||
let mut item = TestItem::new();
|
||||
item.is_dirty = true;
|
||||
item.is_singleton = false;
|
||||
item.project_entry_ids =
|
||||
vec![ProjectEntryId::from_proto(2), ProjectEntryId::from_proto(3)];
|
||||
item
|
||||
};
|
||||
let item_3_4 = {
|
||||
let mut item = TestItem::new();
|
||||
item.is_dirty = true;
|
||||
item.is_singleton = false;
|
||||
item.project_entry_ids =
|
||||
vec![ProjectEntryId::from_proto(3), ProjectEntryId::from_proto(4)];
|
||||
item
|
||||
};
|
||||
|
||||
// Create two panes that contain the following project entries:
|
||||
// left pane:
|
||||
// multi-entry items: (2, 3)
|
||||
// single-entry items: 0, 1, 2, 3, 4
|
||||
// right pane:
|
||||
// single-entry items: 1
|
||||
// multi-entry items: (3, 4)
|
||||
let left_pane = workspace.update(cx, |workspace, cx| {
|
||||
let left_pane = workspace.active_pane().clone();
|
||||
let right_pane = workspace.split_pane(left_pane.clone(), SplitDirection::Right, cx);
|
||||
|
||||
workspace.activate_pane(left_pane.clone(), cx);
|
||||
workspace.add_item(Box::new(cx.add_view(|_| item_2_3.clone())), cx);
|
||||
for item in &single_entry_items {
|
||||
workspace.add_item(Box::new(cx.add_view(|_| item.clone())), cx);
|
||||
}
|
||||
|
||||
workspace.activate_pane(right_pane.clone(), cx);
|
||||
workspace.add_item(Box::new(cx.add_view(|_| single_entry_items[1].clone())), cx);
|
||||
workspace.add_item(Box::new(cx.add_view(|_| item_3_4.clone())), cx);
|
||||
|
||||
left_pane
|
||||
});
|
||||
|
||||
// When closing all of the items in the left pane, we should be prompted twice:
|
||||
// once for project entry 0, and once for project entry 2. After those two
|
||||
// prompts, the task should complete.
|
||||
let close = workspace.update(cx, |workspace, cx| {
|
||||
workspace.activate_pane(left_pane.clone(), cx);
|
||||
Pane::close_items(workspace, left_pane.clone(), cx, |_| true)
|
||||
});
|
||||
|
||||
cx.foreground().run_until_parked();
|
||||
left_pane.read_with(cx, |pane, cx| {
|
||||
assert_eq!(
|
||||
pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
|
||||
&[ProjectEntryId::from_proto(0)]
|
||||
);
|
||||
});
|
||||
cx.simulate_prompt_answer(window_id, 0);
|
||||
|
||||
cx.foreground().run_until_parked();
|
||||
left_pane.read_with(cx, |pane, cx| {
|
||||
assert_eq!(
|
||||
pane.active_item().unwrap().project_entry_ids(cx).as_slice(),
|
||||
&[ProjectEntryId::from_proto(2)]
|
||||
);
|
||||
});
|
||||
cx.simulate_prompt_answer(window_id, 0);
|
||||
|
||||
cx.foreground().run_until_parked();
|
||||
close.await.unwrap();
|
||||
left_pane.read_with(cx, |pane, _| {
|
||||
assert_eq!(pane.items().count(), 0);
|
||||
});
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TestItem {
|
||||
save_count: usize,
|
||||
save_as_count: usize,
|
||||
reload_count: usize,
|
||||
is_dirty: bool,
|
||||
has_conflict: bool,
|
||||
project_entry_ids: Vec<ProjectEntryId>,
|
||||
is_singleton: bool,
|
||||
}
|
||||
|
||||
impl TestItem {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
save_count: 0,
|
||||
save_as_count: 0,
|
||||
reload_count: 0,
|
||||
is_dirty: false,
|
||||
has_conflict: false,
|
||||
project_entry_ids: Vec::new(),
|
||||
is_singleton: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for TestItem {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for TestItem {
|
||||
fn ui_name() -> &'static str {
|
||||
"TestItem"
|
||||
}
|
||||
|
||||
fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
|
||||
Empty::new().boxed()
|
||||
}
|
||||
}
|
||||
|
||||
impl Item for TestItem {
|
||||
fn tab_content(&self, _: &theme::Tab, _: &AppContext) -> ElementBox {
|
||||
Empty::new().boxed()
|
||||
}
|
||||
|
||||
fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
|
||||
None
|
||||
}
|
||||
|
||||
fn project_entry_ids(&self, _: &AppContext) -> SmallVec<[ProjectEntryId; 3]> {
|
||||
self.project_entry_ids.iter().copied().collect()
|
||||
}
|
||||
|
||||
fn is_singleton(&self, _: &AppContext) -> bool {
|
||||
self.is_singleton
|
||||
}
|
||||
|
||||
fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext<Self>) {}
|
||||
|
||||
fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Some(self.clone())
|
||||
}
|
||||
|
||||
fn is_dirty(&self, _: &AppContext) -> bool {
|
||||
self.is_dirty
|
||||
}
|
||||
|
||||
fn has_conflict(&self, _: &AppContext) -> bool {
|
||||
self.has_conflict
|
||||
}
|
||||
|
||||
fn can_save(&self, _: &AppContext) -> bool {
|
||||
self.project_entry_ids.len() > 0
|
||||
}
|
||||
|
||||
fn save(
|
||||
&mut self,
|
||||
_: ModelHandle<Project>,
|
||||
_: &mut ViewContext<Self>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
self.save_count += 1;
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn save_as(
|
||||
&mut self,
|
||||
_: ModelHandle<Project>,
|
||||
_: std::path::PathBuf,
|
||||
_: &mut ViewContext<Self>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
self.save_as_count += 1;
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn reload(
|
||||
&mut self,
|
||||
_: ModelHandle<Project>,
|
||||
_: &mut ViewContext<Self>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
self.reload_count += 1;
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ use std::{
|
||||
};
|
||||
use theme::{ThemeRegistry, DEFAULT_THEME_NAME};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use workspace::{self, AppState, OpenNew, OpenPaths};
|
||||
use workspace::{self, AppState, NewFile, OpenPaths};
|
||||
use zed::{
|
||||
self, build_window_options,
|
||||
fs::RealFs,
|
||||
@ -206,7 +206,7 @@ fn main() {
|
||||
cx.platform().activate(true);
|
||||
let paths = collect_path_args();
|
||||
if paths.is_empty() {
|
||||
cx.dispatch_global_action(OpenNew);
|
||||
cx.dispatch_global_action(NewFile);
|
||||
} else {
|
||||
cx.dispatch_global_action(OpenPaths { paths });
|
||||
}
|
||||
@ -215,7 +215,7 @@ fn main() {
|
||||
cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx))
|
||||
.detach();
|
||||
} else {
|
||||
cx.dispatch_global_action(OpenNew);
|
||||
cx.dispatch_global_action(NewFile);
|
||||
}
|
||||
cx.spawn(|cx| async move {
|
||||
while let Some(connection) = cli_connections_rx.next().await {
|
||||
|
@ -31,21 +31,41 @@ pub fn menus() -> Vec<Menu<'static>> {
|
||||
items: vec![
|
||||
MenuItem::Action {
|
||||
name: "New",
|
||||
action: Box::new(workspace::OpenNew),
|
||||
action: Box::new(workspace::NewFile),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "New Window",
|
||||
action: Box::new(workspace::NewWindow),
|
||||
},
|
||||
MenuItem::Separator,
|
||||
MenuItem::Action {
|
||||
name: "Open…",
|
||||
action: Box::new(workspace::Open),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Add Folder to Project…",
|
||||
action: Box::new(workspace::AddFolderToProject),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Save",
|
||||
action: Box::new(workspace::Save),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Save As…",
|
||||
action: Box::new(workspace::SaveAs),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Save All",
|
||||
action: Box::new(workspace::SaveAll),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Close Editor",
|
||||
action: Box::new(workspace::CloseActiveItem),
|
||||
},
|
||||
MenuItem::Action {
|
||||
name: "Close Window",
|
||||
action: Box::new(workspace::CloseWindow),
|
||||
},
|
||||
],
|
||||
},
|
||||
Menu {
|
||||
@ -209,5 +229,12 @@ pub fn menus() -> Vec<Menu<'static>> {
|
||||
},
|
||||
],
|
||||
},
|
||||
Menu {
|
||||
name: "Help",
|
||||
items: vec![MenuItem::Action {
|
||||
name: "Command Palette",
|
||||
action: Box::new(command_palette::Toggle),
|
||||
}],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
@ -314,7 +314,7 @@ mod tests {
|
||||
};
|
||||
use theme::{Theme, ThemeRegistry, DEFAULT_THEME_NAME};
|
||||
use workspace::{
|
||||
open_paths, pane, Item, ItemHandle, OpenNew, Pane, SplitDirection, WorkspaceHandle,
|
||||
open_paths, pane, Item, ItemHandle, NewFile, Pane, SplitDirection, WorkspaceHandle,
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
@ -376,7 +376,7 @@ mod tests {
|
||||
#[gpui::test]
|
||||
async fn test_new_empty_workspace(cx: &mut TestAppContext) {
|
||||
let app_state = init(cx);
|
||||
cx.dispatch_global_action(workspace::OpenNew);
|
||||
cx.dispatch_global_action(workspace::NewFile);
|
||||
let window_id = *cx.window_ids().first().unwrap();
|
||||
let workspace = cx.root_view::<Workspace>(window_id).unwrap();
|
||||
let editor = workspace.update(cx, |workspace, cx| {
|
||||
@ -391,7 +391,7 @@ mod tests {
|
||||
assert!(editor.text(cx).is_empty());
|
||||
});
|
||||
|
||||
let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(cx));
|
||||
let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
|
||||
app_state.fs.as_fake().insert_dir("/root").await;
|
||||
cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
|
||||
save_task.await.unwrap();
|
||||
@ -666,7 +666,7 @@ mod tests {
|
||||
.await;
|
||||
cx.read(|cx| assert!(editor.is_dirty(cx)));
|
||||
|
||||
let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(cx));
|
||||
let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
|
||||
cx.simulate_prompt_answer(window_id, 0);
|
||||
save_task.await.unwrap();
|
||||
editor.read_with(cx, |editor, cx| {
|
||||
@ -686,7 +686,7 @@ mod tests {
|
||||
let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap());
|
||||
|
||||
// Create a new untitled buffer
|
||||
cx.dispatch_action(window_id, OpenNew);
|
||||
cx.dispatch_action(window_id, NewFile);
|
||||
let editor = workspace.read_with(cx, |workspace, cx| {
|
||||
workspace
|
||||
.active_item(cx)
|
||||
@ -707,7 +707,7 @@ mod tests {
|
||||
});
|
||||
|
||||
// Save the buffer. This prompts for a filename.
|
||||
let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(cx));
|
||||
let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
|
||||
cx.simulate_new_path_selection(|parent_dir| {
|
||||
assert_eq!(parent_dir, Path::new("/root"));
|
||||
Some(parent_dir.join("the-new-name.rs"))
|
||||
@ -731,7 +731,7 @@ mod tests {
|
||||
editor.handle_input(&editor::Input(" there".into()), cx);
|
||||
assert_eq!(editor.is_dirty(cx.as_ref()), true);
|
||||
});
|
||||
let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(cx));
|
||||
let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
|
||||
save_task.await.unwrap();
|
||||
assert!(!cx.did_prompt_for_new_path());
|
||||
editor.read_with(cx, |editor, cx| {
|
||||
@ -741,7 +741,7 @@ mod tests {
|
||||
|
||||
// Open the same newly-created file in another pane item. The new editor should reuse
|
||||
// the same buffer.
|
||||
cx.dispatch_action(window_id, OpenNew);
|
||||
cx.dispatch_action(window_id, NewFile);
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx);
|
||||
@ -774,7 +774,7 @@ mod tests {
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
|
||||
|
||||
// Create a new untitled buffer
|
||||
cx.dispatch_action(window_id, OpenNew);
|
||||
cx.dispatch_action(window_id, NewFile);
|
||||
let editor = workspace.read_with(cx, |workspace, cx| {
|
||||
workspace
|
||||
.active_item(cx)
|
||||
@ -793,7 +793,7 @@ mod tests {
|
||||
});
|
||||
|
||||
// Save the buffer. This prompts for a filename.
|
||||
let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(cx));
|
||||
let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
|
||||
cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs")));
|
||||
save_task.await.unwrap();
|
||||
// The buffer is not dirty anymore and the language is assigned based on the path.
|
||||
|
Loading…
Reference in New Issue
Block a user