mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-25 22:18:14 +03:00
Restore unsaved buffers on restart (#13546)
This adds the ability for Zed to restore unsaved buffers on restart. The user is no longer prompted to save/discard/cancel when trying to close a Zed window with dirty buffers in it. Instead those dirty buffers are stored and restored on restart. It does this by saving the contents of dirty buffers to the internal SQLite database in which Zed stores other data too. On restart, if there are dirty buffers in the database, they are restored. On certain events (buffer changed, file saved, ...) Zed will serialize these buffers, throttled to a 100ms, so that we don't overload the machine by saving on every keystroke. When Zed quits, it waits until all the buffers are serialized. ### Current limitations - It does not persist undo-history (right now we don't persist/restore undo-history regardless of dirty buffers or not) - It does not restore buffers in windows without projects/worktrees. Example: if you open a new window with `cmd-shift-n` and type something in a buffer, this will _not_ be stored and you will be asked whether to save/discard on quit. In the future, we want to fix this by also restoring windows without projects/worktrees. ### Demo https://github.com/user-attachments/assets/45c63237-8848-471f-8575-ac05496bba19 ### Related tickets I'm unsure about closing them, without also fixing the 2nd limitation: restoring of worktree-less windows. So let's wait until that. - https://github.com/zed-industries/zed/issues/4985 - https://github.com/zed-industries/zed/issues/4683 ### Note on performance - Serializing editing buffer (asynchronously on background thread) with 500k lines takes ~200ms on M3 Max. That's an extreme case and that performance seems acceptable. Release Notes: - Added automatic restoring of unsaved buffers. Zed can now be closed even if there are unsaved changes in buffers. One current limitation is that this only works when having projects open, not single files or empty windows with unsaved buffers. The feature can be turned off by setting `{"session": {"restore_unsaved_buffers": false}}`. --------- Co-authored-by: Bennet <bennet@zed.dev> Co-authored-by: Antonio <antonio@zed.dev>
This commit is contained in:
parent
8e9e94de22
commit
9241b11e1f
10
Cargo.lock
generated
10
Cargo.lock
generated
@ -5493,7 +5493,6 @@ dependencies = [
|
|||||||
"gpui",
|
"gpui",
|
||||||
"project",
|
"project",
|
||||||
"ui",
|
"ui",
|
||||||
"util",
|
|
||||||
"workspace",
|
"workspace",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -8536,6 +8535,7 @@ dependencies = [
|
|||||||
"rpc",
|
"rpc",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"settings",
|
||||||
"smol",
|
"smol",
|
||||||
"task",
|
"task",
|
||||||
"terminal_view",
|
"terminal_view",
|
||||||
@ -10919,18 +10919,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.61"
|
version = "1.0.62"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
|
checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror-impl",
|
"thiserror-impl",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror-impl"
|
name = "thiserror-impl"
|
||||||
version = "1.0.61"
|
version = "1.0.62"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
|
checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -45,7 +45,7 @@ use ui::{h_flex, prelude::*, Icon, IconName, Label};
|
|||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use workspace::{
|
use workspace::{
|
||||||
item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams},
|
item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams},
|
||||||
ItemNavHistory, Pane, ToolbarItemLocation, Workspace,
|
ItemNavHistory, ToolbarItemLocation, Workspace,
|
||||||
};
|
};
|
||||||
|
|
||||||
actions!(diagnostics, [Deploy, ToggleWarnings]);
|
actions!(diagnostics, [Deploy, ToggleWarnings]);
|
||||||
@ -786,20 +786,6 @@ impl Item for ProjectDiagnosticsEditor {
|
|||||||
self.editor
|
self.editor
|
||||||
.update(cx, |editor, cx| editor.added_to_workspace(workspace, cx));
|
.update(cx, |editor, cx| editor.added_to_workspace(workspace, cx));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn serialized_item_kind() -> Option<&'static str> {
|
|
||||||
Some("diagnostics")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize(
|
|
||||||
project: Model<Project>,
|
|
||||||
workspace: WeakView<Workspace>,
|
|
||||||
_workspace_id: workspace::WorkspaceId,
|
|
||||||
_item_id: workspace::ItemId,
|
|
||||||
cx: &mut ViewContext<Pane>,
|
|
||||||
) -> Task<Result<View<Self>>> {
|
|
||||||
Task::ready(Ok(cx.new_view(|cx| Self::new(project, workspace, cx))))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const DIAGNOSTIC_HEADER: &'static str = "diagnostic header";
|
const DIAGNOSTIC_HEADER: &'static str = "diagnostic header";
|
||||||
|
@ -40,7 +40,7 @@ use ui::{h_flex, prelude::*, Icon, IconName, Label};
|
|||||||
use util::{debug_panic, ResultExt};
|
use util::{debug_panic, ResultExt};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams},
|
item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams},
|
||||||
ItemNavHistory, Pane, ToolbarItemLocation, Workspace,
|
ItemNavHistory, ToolbarItemLocation, Workspace,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::project_diagnostics_settings::ProjectDiagnosticsSettings;
|
use crate::project_diagnostics_settings::ProjectDiagnosticsSettings;
|
||||||
@ -603,20 +603,6 @@ impl Item for GroupedDiagnosticsEditor {
|
|||||||
self.editor
|
self.editor
|
||||||
.update(cx, |editor, cx| editor.added_to_workspace(workspace, cx));
|
.update(cx, |editor, cx| editor.added_to_workspace(workspace, cx));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn serialized_item_kind() -> Option<&'static str> {
|
|
||||||
Some("diagnostics")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize(
|
|
||||||
project: Model<Project>,
|
|
||||||
workspace: WeakView<Workspace>,
|
|
||||||
_workspace_id: workspace::WorkspaceId,
|
|
||||||
_item_id: workspace::ItemId,
|
|
||||||
cx: &mut ViewContext<Pane>,
|
|
||||||
) -> Task<Result<View<Self>>> {
|
|
||||||
Task::ready(Ok(cx.new_view(|cx| Self::new(project, workspace, cx))))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn compare_data_locations(
|
fn compare_data_locations(
|
||||||
|
@ -272,7 +272,8 @@ pub fn init(cx: &mut AppContext) {
|
|||||||
|
|
||||||
workspace::register_project_item::<Editor>(cx);
|
workspace::register_project_item::<Editor>(cx);
|
||||||
workspace::FollowableViewRegistry::register::<Editor>(cx);
|
workspace::FollowableViewRegistry::register::<Editor>(cx);
|
||||||
workspace::register_deserializable_item::<Editor>(cx);
|
workspace::register_serializable_item::<Editor>(cx);
|
||||||
|
|
||||||
cx.observe_new_views(
|
cx.observe_new_views(
|
||||||
|workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
|
|workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
|
||||||
workspace.register_action(Editor::new_file);
|
workspace.register_action(Editor::new_file);
|
||||||
@ -550,6 +551,7 @@ pub struct Editor {
|
|||||||
show_git_blame_inline: bool,
|
show_git_blame_inline: bool,
|
||||||
show_git_blame_inline_delay_task: Option<Task<()>>,
|
show_git_blame_inline_delay_task: Option<Task<()>>,
|
||||||
git_blame_inline_enabled: bool,
|
git_blame_inline_enabled: bool,
|
||||||
|
serialize_dirty_buffers: bool,
|
||||||
show_selection_menu: Option<bool>,
|
show_selection_menu: Option<bool>,
|
||||||
blame: Option<Model<GitBlame>>,
|
blame: Option<Model<GitBlame>>,
|
||||||
blame_subscription: Option<Subscription>,
|
blame_subscription: Option<Subscription>,
|
||||||
@ -1876,6 +1878,9 @@ impl Editor {
|
|||||||
show_selection_menu: None,
|
show_selection_menu: None,
|
||||||
show_git_blame_inline_delay_task: None,
|
show_git_blame_inline_delay_task: None,
|
||||||
git_blame_inline_enabled: ProjectSettings::get_global(cx).git.inline_blame_enabled(),
|
git_blame_inline_enabled: ProjectSettings::get_global(cx).git.inline_blame_enabled(),
|
||||||
|
serialize_dirty_buffers: ProjectSettings::get_global(cx)
|
||||||
|
.session
|
||||||
|
.restore_unsaved_buffers,
|
||||||
blame: None,
|
blame: None,
|
||||||
blame_subscription: None,
|
blame_subscription: None,
|
||||||
file_header_size,
|
file_header_size,
|
||||||
@ -11250,8 +11255,11 @@ impl Editor {
|
|||||||
self.scroll_manager.vertical_scroll_margin = editor_settings.vertical_scroll_margin;
|
self.scroll_manager.vertical_scroll_margin = editor_settings.vertical_scroll_margin;
|
||||||
self.show_breadcrumbs = editor_settings.toolbar.breadcrumbs;
|
self.show_breadcrumbs = editor_settings.toolbar.breadcrumbs;
|
||||||
|
|
||||||
|
let project_settings = ProjectSettings::get_global(cx);
|
||||||
|
self.serialize_dirty_buffers = project_settings.session.restore_unsaved_buffers;
|
||||||
|
|
||||||
if self.mode == EditorMode::Full {
|
if self.mode == EditorMode::Full {
|
||||||
let inline_blame_enabled = ProjectSettings::get_global(cx).git.inline_blame_enabled();
|
let inline_blame_enabled = project_settings.git.inline_blame_enabled();
|
||||||
if self.git_blame_inline_enabled != inline_blame_enabled {
|
if self.git_blame_inline_enabled != inline_blame_enabled {
|
||||||
self.toggle_git_blame_inline_internal(false, cx);
|
self.toggle_git_blame_inline_internal(false, cx);
|
||||||
}
|
}
|
||||||
|
@ -16,10 +16,13 @@ use language::{
|
|||||||
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, Point, SelectionGoal,
|
proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, CharKind, Point, SelectionGoal,
|
||||||
};
|
};
|
||||||
use multi_buffer::AnchorRangeExt;
|
use multi_buffer::AnchorRangeExt;
|
||||||
use project::{search::SearchQuery, FormatTrigger, Item as _, Project, ProjectPath};
|
use project::{
|
||||||
|
project_settings::ProjectSettings, search::SearchQuery, FormatTrigger, Item as _, Project,
|
||||||
|
ProjectPath,
|
||||||
|
};
|
||||||
use rpc::proto::{self, update_view, PeerId};
|
use rpc::proto::{self, update_view, PeerId};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use workspace::item::{Dedup, ItemSettings, TabContentParams};
|
use workspace::item::{Dedup, ItemSettings, SerializableItem, TabContentParams};
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
any::TypeId,
|
any::TypeId,
|
||||||
@ -36,7 +39,7 @@ use ui::{h_flex, prelude::*, Label};
|
|||||||
use util::{paths::PathExt, ResultExt, TryFutureExt};
|
use util::{paths::PathExt, ResultExt, TryFutureExt};
|
||||||
use workspace::item::{BreadcrumbText, FollowEvent};
|
use workspace::item::{BreadcrumbText, FollowEvent};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem},
|
item::{FollowableItem, Item, ItemEvent, ProjectItem},
|
||||||
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
|
searchable::{Direction, SearchEvent, SearchableItem, SearchableItemHandle},
|
||||||
ItemId, ItemNavHistory, Pane, ToolbarItemLocation, ViewId, Workspace, WorkspaceId,
|
ItemId, ItemNavHistory, Pane, ToolbarItemLocation, ViewId, Workspace, WorkspaceId,
|
||||||
};
|
};
|
||||||
@ -837,54 +840,8 @@ impl Item for Editor {
|
|||||||
Some(breadcrumbs)
|
Some(breadcrumbs)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
|
fn added_to_workspace(&mut self, workspace: &mut Workspace, _: &mut ViewContext<Self>) {
|
||||||
self.workspace = Some((workspace.weak_handle(), workspace.database_id()));
|
self.workspace = Some((workspace.weak_handle(), workspace.database_id()));
|
||||||
let Some(workspace_id) = workspace.database_id() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let item_id = cx.view().item_id().as_u64() as ItemId;
|
|
||||||
|
|
||||||
fn serialize(
|
|
||||||
buffer: Model<Buffer>,
|
|
||||||
workspace_id: WorkspaceId,
|
|
||||||
item_id: ItemId,
|
|
||||||
cx: &mut AppContext,
|
|
||||||
) {
|
|
||||||
if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) {
|
|
||||||
let path = file.abs_path(cx);
|
|
||||||
|
|
||||||
cx.background_executor()
|
|
||||||
.spawn(async move {
|
|
||||||
DB.save_path(item_id, workspace_id, path.clone())
|
|
||||||
.await
|
|
||||||
.log_err()
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(buffer) = self.buffer().read(cx).as_singleton() {
|
|
||||||
serialize(buffer.clone(), workspace_id, item_id, cx);
|
|
||||||
|
|
||||||
cx.subscribe(&buffer, |this, buffer, event, cx| {
|
|
||||||
if let Some((_, Some(workspace_id))) = this.workspace.as_ref() {
|
|
||||||
if let language::Event::FileHandleChanged = event {
|
|
||||||
serialize(
|
|
||||||
buffer,
|
|
||||||
*workspace_id,
|
|
||||||
cx.view().item_id().as_u64() as ItemId,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serialized_item_kind() -> Option<&'static str> {
|
|
||||||
Some("Editor")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_item_events(event: &EditorEvent, mut f: impl FnMut(ItemEvent)) {
|
fn to_item_events(event: &EditorEvent, mut f: impl FnMut(ItemEvent)) {
|
||||||
@ -920,6 +877,20 @@ impl Item for Editor {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SerializableItem for Editor {
|
||||||
|
fn serialized_item_kind() -> &'static str {
|
||||||
|
"Editor"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cleanup(
|
||||||
|
workspace_id: WorkspaceId,
|
||||||
|
alive_items: Vec<ItemId>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> Task<Result<()>> {
|
||||||
|
cx.spawn(|_| DB.delete_unloaded_items(workspace_id, alive_items))
|
||||||
|
}
|
||||||
|
|
||||||
fn deserialize(
|
fn deserialize(
|
||||||
project: Model<Project>,
|
project: Model<Project>,
|
||||||
@ -928,12 +899,57 @@ impl Item for Editor {
|
|||||||
item_id: ItemId,
|
item_id: ItemId,
|
||||||
cx: &mut ViewContext<Pane>,
|
cx: &mut ViewContext<Pane>,
|
||||||
) -> Task<Result<View<Self>>> {
|
) -> Task<Result<View<Self>>> {
|
||||||
let project_item: Result<_> = project.update(cx, |project, cx| {
|
let path_content_language = match DB
|
||||||
// Look up the path with this key associated, create a self with that path
|
.get_path_and_contents(item_id, workspace_id)
|
||||||
let path = DB
|
.context("Failed to query editor state")
|
||||||
.get_path(item_id, workspace_id)?
|
{
|
||||||
.context("No path stored for this editor")?;
|
Ok(Some((path, content, language))) => {
|
||||||
|
if ProjectSettings::get_global(cx)
|
||||||
|
.session
|
||||||
|
.restore_unsaved_buffers
|
||||||
|
{
|
||||||
|
(path, content, language)
|
||||||
|
} else {
|
||||||
|
(path, None, None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None) => {
|
||||||
|
return Task::ready(Err(anyhow!("No path or contents found for buffer")));
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
return Task::ready(Err(error));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match path_content_language {
|
||||||
|
(None, Some(content), language_name) => cx.spawn(|_, mut cx| async move {
|
||||||
|
let language = if let Some(language_name) = language_name {
|
||||||
|
let language_registry =
|
||||||
|
project.update(&mut cx, |project, _| project.languages().clone())?;
|
||||||
|
|
||||||
|
Some(language_registry.language_for_name(&language_name).await?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
// First create the empty buffer
|
||||||
|
let buffer = project.update(&mut cx, |project, cx| {
|
||||||
|
project.create_local_buffer("", language, cx)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Then set the text so that the dirty bit is set correctly
|
||||||
|
buffer.update(&mut cx, |buffer, cx| {
|
||||||
|
buffer.set_text(content, cx);
|
||||||
|
})?;
|
||||||
|
|
||||||
|
cx.new_view(|cx| {
|
||||||
|
let mut editor = Editor::for_buffer(buffer, Some(project), cx);
|
||||||
|
editor.read_scroll_position_from_db(item_id, workspace_id, cx);
|
||||||
|
editor
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
(Some(path), contents, _) => {
|
||||||
|
let project_item = project.update(cx, |project, cx| {
|
||||||
let (worktree, path) = project
|
let (worktree, path) = project
|
||||||
.find_worktree(&path, cx)
|
.find_worktree(&path, cx)
|
||||||
.with_context(|| format!("No worktree for path: {path:?}"))?;
|
.with_context(|| format!("No worktree for path: {path:?}"))?;
|
||||||
@ -949,9 +965,21 @@ impl Item for Editor {
|
|||||||
.map(|project_item| {
|
.map(|project_item| {
|
||||||
cx.spawn(|pane, mut cx| async move {
|
cx.spawn(|pane, mut cx| async move {
|
||||||
let (_, project_item) = project_item.await?;
|
let (_, project_item) = project_item.await?;
|
||||||
let buffer = project_item
|
let buffer = project_item.downcast::<Buffer>().map_err(|_| {
|
||||||
.downcast::<Buffer>()
|
anyhow!("Project item at stored path was not a buffer")
|
||||||
.map_err(|_| anyhow!("Project item at stored path was not a buffer"))?;
|
})?;
|
||||||
|
|
||||||
|
// This is a bit wasteful: we're loading the whole buffer from
|
||||||
|
// disk and then overwrite the content.
|
||||||
|
// But for now, it keeps the implementation of the content serialization
|
||||||
|
// simple, because we don't have to persist all of the metadata that we get
|
||||||
|
// by loading the file (git diff base, mtime, ...).
|
||||||
|
if let Some(buffer_text) = contents {
|
||||||
|
buffer.update(&mut cx, |buffer, cx| {
|
||||||
|
buffer.set_text(buffer_text, cx);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
pane.update(&mut cx, |_, cx| {
|
pane.update(&mut cx, |_, cx| {
|
||||||
cx.new_view(|cx| {
|
cx.new_view(|cx| {
|
||||||
let mut editor = Editor::for_buffer(buffer, Some(project), cx);
|
let mut editor = Editor::for_buffer(buffer, Some(project), cx);
|
||||||
@ -964,6 +992,79 @@ impl Item for Editor {
|
|||||||
})
|
})
|
||||||
.unwrap_or_else(|error| Task::ready(Err(error)))
|
.unwrap_or_else(|error| Task::ready(Err(error)))
|
||||||
}
|
}
|
||||||
|
_ => Task::ready(Err(anyhow!("No path or contents found for buffer"))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialize(
|
||||||
|
&mut self,
|
||||||
|
workspace: &mut Workspace,
|
||||||
|
item_id: ItemId,
|
||||||
|
closing: bool,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Option<Task<Result<()>>> {
|
||||||
|
let mut serialize_dirty_buffers = self.serialize_dirty_buffers;
|
||||||
|
|
||||||
|
let project = self.project.clone()?;
|
||||||
|
if project.read(cx).visible_worktrees(cx).next().is_none() {
|
||||||
|
// If we don't have a worktree, we don't serialize, because
|
||||||
|
// projects without worktrees aren't deserialized.
|
||||||
|
serialize_dirty_buffers = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if closing && !serialize_dirty_buffers {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let workspace_id = workspace.database_id()?;
|
||||||
|
|
||||||
|
let buffer = self.buffer().read(cx).as_singleton()?;
|
||||||
|
|
||||||
|
let is_dirty = buffer.read(cx).is_dirty();
|
||||||
|
let path = buffer
|
||||||
|
.read(cx)
|
||||||
|
.file()
|
||||||
|
.and_then(|file| file.as_local())
|
||||||
|
.map(|file| file.abs_path(cx));
|
||||||
|
let snapshot = buffer.read(cx).snapshot();
|
||||||
|
|
||||||
|
Some(cx.spawn(|_this, cx| async move {
|
||||||
|
cx.background_executor()
|
||||||
|
.spawn(async move {
|
||||||
|
if let Some(path) = path {
|
||||||
|
DB.save_path(item_id, workspace_id, path.clone())
|
||||||
|
.await
|
||||||
|
.context("failed to save path of buffer")?
|
||||||
|
}
|
||||||
|
|
||||||
|
if serialize_dirty_buffers {
|
||||||
|
let (contents, language) = if is_dirty {
|
||||||
|
let contents = snapshot.text();
|
||||||
|
let language = snapshot.language().map(|lang| lang.name().to_string());
|
||||||
|
(Some(contents), language)
|
||||||
|
} else {
|
||||||
|
(None, None)
|
||||||
|
};
|
||||||
|
|
||||||
|
DB.save_contents(item_id, workspace_id, contents, language)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
anyhow::Ok(())
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.context("failed to save contents of buffer")?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_serialize(&self, event: &Self::Event) -> bool {
|
||||||
|
matches!(
|
||||||
|
event,
|
||||||
|
EditorEvent::Saved | EditorEvent::DirtyChanged | EditorEvent::BufferEdited
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProjectItem for Editor {
|
impl ProjectItem for Editor {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use db::sqlez::statement::Statement;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use db::sqlez_macros::sql;
|
use db::sqlez_macros::sql;
|
||||||
@ -10,10 +12,12 @@ define_connection!(
|
|||||||
// editors(
|
// editors(
|
||||||
// item_id: usize,
|
// item_id: usize,
|
||||||
// workspace_id: usize,
|
// workspace_id: usize,
|
||||||
// path: PathBuf,
|
// path: Option<PathBuf>,
|
||||||
// scroll_top_row: usize,
|
// scroll_top_row: usize,
|
||||||
// scroll_vertical_offset: f32,
|
// scroll_vertical_offset: f32,
|
||||||
// scroll_horizontal_offset: f32,
|
// scroll_horizontal_offset: f32,
|
||||||
|
// content: Option<String>,
|
||||||
|
// language: Option<String>,
|
||||||
// )
|
// )
|
||||||
pub static ref DB: EditorDb<WorkspaceDb> =
|
pub static ref DB: EditorDb<WorkspaceDb> =
|
||||||
&[sql! (
|
&[sql! (
|
||||||
@ -31,13 +35,39 @@ define_connection!(
|
|||||||
ALTER TABLE editors ADD COLUMN scroll_top_row INTEGER NOT NULL DEFAULT 0;
|
ALTER TABLE editors ADD COLUMN scroll_top_row INTEGER NOT NULL DEFAULT 0;
|
||||||
ALTER TABLE editors ADD COLUMN scroll_horizontal_offset REAL NOT NULL DEFAULT 0;
|
ALTER TABLE editors ADD COLUMN scroll_horizontal_offset REAL NOT NULL DEFAULT 0;
|
||||||
ALTER TABLE editors ADD COLUMN scroll_vertical_offset REAL NOT NULL DEFAULT 0;
|
ALTER TABLE editors ADD COLUMN scroll_vertical_offset REAL NOT NULL DEFAULT 0;
|
||||||
|
),
|
||||||
|
sql! (
|
||||||
|
// Since sqlite3 doesn't support ALTER COLUMN, we create a new
|
||||||
|
// table, move the data over, drop the old table, rename new table.
|
||||||
|
CREATE TABLE new_editors_tmp (
|
||||||
|
item_id INTEGER NOT NULL,
|
||||||
|
workspace_id INTEGER NOT NULL,
|
||||||
|
path BLOB, // <-- No longer "NOT NULL"
|
||||||
|
scroll_top_row INTEGER NOT NULL DEFAULT 0,
|
||||||
|
scroll_horizontal_offset REAL NOT NULL DEFAULT 0,
|
||||||
|
scroll_vertical_offset REAL NOT NULL DEFAULT 0,
|
||||||
|
contents TEXT, // New
|
||||||
|
language TEXT, // New
|
||||||
|
PRIMARY KEY(item_id, workspace_id),
|
||||||
|
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
ON UPDATE CASCADE
|
||||||
|
) STRICT;
|
||||||
|
|
||||||
|
INSERT INTO new_editors_tmp(item_id, workspace_id, path, scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset)
|
||||||
|
SELECT item_id, workspace_id, path, scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset
|
||||||
|
FROM editors;
|
||||||
|
|
||||||
|
DROP TABLE editors;
|
||||||
|
|
||||||
|
ALTER TABLE new_editors_tmp RENAME TO editors;
|
||||||
)];
|
)];
|
||||||
);
|
);
|
||||||
|
|
||||||
impl EditorDb {
|
impl EditorDb {
|
||||||
query! {
|
query! {
|
||||||
pub fn get_path(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<PathBuf>> {
|
pub fn get_path_and_contents(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<(Option<PathBuf>, Option<String>, Option<String>)>> {
|
||||||
SELECT path FROM editors
|
SELECT path, contents, language FROM editors
|
||||||
WHERE item_id = ? AND workspace_id = ?
|
WHERE item_id = ? AND workspace_id = ?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -55,6 +85,20 @@ impl EditorDb {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
query! {
|
||||||
|
pub async fn save_contents(item_id: ItemId, workspace: WorkspaceId, contents: Option<String>, language: Option<String>) -> Result<()> {
|
||||||
|
INSERT INTO editors
|
||||||
|
(item_id, workspace_id, contents, language)
|
||||||
|
VALUES
|
||||||
|
(?1, ?2, ?3, ?4)
|
||||||
|
ON CONFLICT DO UPDATE SET
|
||||||
|
item_id = ?1,
|
||||||
|
workspace_id = ?2,
|
||||||
|
contents = ?3,
|
||||||
|
language = ?4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Returns the scroll top row, and offset
|
// Returns the scroll top row, and offset
|
||||||
query! {
|
query! {
|
||||||
pub fn get_scroll_position(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<(u32, f32, f32)>> {
|
pub fn get_scroll_position(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<(u32, f32, f32)>> {
|
||||||
@ -80,4 +124,75 @@ impl EditorDb {
|
|||||||
WHERE item_id = ?1 AND workspace_id = ?2
|
WHERE item_id = ?1 AND workspace_id = ?2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn delete_unloaded_items(
|
||||||
|
&self,
|
||||||
|
workspace: WorkspaceId,
|
||||||
|
alive_items: Vec<ItemId>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let placeholders = alive_items
|
||||||
|
.iter()
|
||||||
|
.map(|_| "?")
|
||||||
|
.collect::<Vec<&str>>()
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
|
let query = format!(
|
||||||
|
"DELETE FROM editors WHERE workspace_id = ? AND item_id NOT IN ({placeholders})"
|
||||||
|
);
|
||||||
|
|
||||||
|
self.write(move |conn| {
|
||||||
|
let mut statement = Statement::prepare(conn, query)?;
|
||||||
|
let mut next_index = statement.bind(&workspace, 1)?;
|
||||||
|
for id in alive_items {
|
||||||
|
next_index = statement.bind(&id, next_index)?;
|
||||||
|
}
|
||||||
|
statement.exec()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use gpui;
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_saving_content() {
|
||||||
|
env_logger::try_init().ok();
|
||||||
|
|
||||||
|
let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
|
||||||
|
|
||||||
|
// Sanity check: make sure there is no row in the `editors` table
|
||||||
|
assert_eq!(DB.get_path_and_contents(1234, workspace_id).unwrap(), None);
|
||||||
|
|
||||||
|
// Save content/language
|
||||||
|
DB.save_contents(
|
||||||
|
1234,
|
||||||
|
workspace_id,
|
||||||
|
Some("testing".into()),
|
||||||
|
Some("Go".into()),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Check that it can be read from DB
|
||||||
|
let path_and_contents = DB.get_path_and_contents(1234, workspace_id).unwrap();
|
||||||
|
let (path, contents, language) = path_and_contents.unwrap();
|
||||||
|
assert!(path.is_none());
|
||||||
|
assert_eq!(contents, Some("testing".to_owned()));
|
||||||
|
assert_eq!(language, Some("Go".to_owned()));
|
||||||
|
|
||||||
|
// Update it with NULL
|
||||||
|
DB.save_contents(1234, workspace_id, None, None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Check that it worked
|
||||||
|
let path_and_contents = DB.get_path_and_contents(1234, workspace_id).unwrap();
|
||||||
|
let (path, contents, language) = path_and_contents.unwrap();
|
||||||
|
assert!(path.is_none());
|
||||||
|
assert!(contents.is_none());
|
||||||
|
assert!(language.is_none());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,5 @@ anyhow.workspace = true
|
|||||||
db.workspace = true
|
db.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
ui.workspace = true
|
ui.workspace = true
|
||||||
util.workspace = true
|
|
||||||
workspace.workspace = true
|
workspace.workspace = true
|
||||||
project.workspace = true
|
project.workspace = true
|
||||||
|
@ -9,9 +9,8 @@ use ui::prelude::*;
|
|||||||
|
|
||||||
use project::{Project, ProjectEntryId, ProjectPath};
|
use project::{Project, ProjectEntryId, ProjectPath};
|
||||||
use std::{ffi::OsStr, path::PathBuf};
|
use std::{ffi::OsStr, path::PathBuf};
|
||||||
use util::ResultExt;
|
|
||||||
use workspace::{
|
use workspace::{
|
||||||
item::{Item, ProjectItem, TabContentParams},
|
item::{Item, ProjectItem, SerializableItem, TabContentParams},
|
||||||
ItemId, Pane, Workspace, WorkspaceId,
|
ItemId, Pane, Workspace, WorkspaceId,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -90,49 +89,6 @@ impl Item for ImageView {
|
|||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
|
|
||||||
let item_id = cx.entity_id().as_u64();
|
|
||||||
let workspace_id = workspace.database_id();
|
|
||||||
let image_path = self.path.clone();
|
|
||||||
|
|
||||||
if let Some(workspace_id) = workspace_id {
|
|
||||||
cx.background_executor()
|
|
||||||
.spawn({
|
|
||||||
let image_path = image_path.clone();
|
|
||||||
async move {
|
|
||||||
IMAGE_VIEWER
|
|
||||||
.save_image_path(item_id, workspace_id, image_path)
|
|
||||||
.await
|
|
||||||
.log_err();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serialized_item_kind() -> Option<&'static str> {
|
|
||||||
Some(IMAGE_VIEWER_KIND)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize(
|
|
||||||
_project: Model<Project>,
|
|
||||||
_workspace: WeakView<Workspace>,
|
|
||||||
workspace_id: WorkspaceId,
|
|
||||||
item_id: ItemId,
|
|
||||||
cx: &mut ViewContext<Pane>,
|
|
||||||
) -> Task<anyhow::Result<View<Self>>> {
|
|
||||||
cx.spawn(|_pane, mut cx| async move {
|
|
||||||
let image_path = IMAGE_VIEWER
|
|
||||||
.get_image_path(item_id, workspace_id)?
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("No image path found"))?;
|
|
||||||
|
|
||||||
cx.new_view(|cx| ImageView {
|
|
||||||
path: image_path,
|
|
||||||
focus_handle: cx.focus_handle(),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn clone_on_split(
|
fn clone_on_split(
|
||||||
&self,
|
&self,
|
||||||
_workspace_id: Option<WorkspaceId>,
|
_workspace_id: Option<WorkspaceId>,
|
||||||
@ -148,6 +104,62 @@ impl Item for ImageView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl SerializableItem for ImageView {
|
||||||
|
fn serialized_item_kind() -> &'static str {
|
||||||
|
IMAGE_VIEWER_KIND
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize(
|
||||||
|
_project: Model<Project>,
|
||||||
|
_workspace: WeakView<Workspace>,
|
||||||
|
workspace_id: WorkspaceId,
|
||||||
|
item_id: ItemId,
|
||||||
|
cx: &mut ViewContext<Pane>,
|
||||||
|
) -> Task<gpui::Result<View<Self>>> {
|
||||||
|
cx.spawn(|_pane, mut cx| async move {
|
||||||
|
let image_path = IMAGE_VIEWER
|
||||||
|
.get_image_path(item_id, workspace_id)?
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("No image path found"))?;
|
||||||
|
|
||||||
|
cx.new_view(|cx| ImageView {
|
||||||
|
path: image_path,
|
||||||
|
focus_handle: cx.focus_handle(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cleanup(
|
||||||
|
workspace_id: WorkspaceId,
|
||||||
|
alive_items: Vec<ItemId>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> Task<gpui::Result<()>> {
|
||||||
|
cx.spawn(|_| IMAGE_VIEWER.delete_unloaded_items(workspace_id, alive_items))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialize(
|
||||||
|
&mut self,
|
||||||
|
workspace: &mut Workspace,
|
||||||
|
item_id: ItemId,
|
||||||
|
_closing: bool,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Option<Task<gpui::Result<()>>> {
|
||||||
|
let workspace_id = workspace.database_id()?;
|
||||||
|
|
||||||
|
Some(cx.background_executor().spawn({
|
||||||
|
let image_path = self.path.clone();
|
||||||
|
async move {
|
||||||
|
IMAGE_VIEWER
|
||||||
|
.save_image_path(item_id, workspace_id, image_path)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_serialize(&self, _event: &Self::Event) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl EventEmitter<()> for ImageView {}
|
impl EventEmitter<()> for ImageView {}
|
||||||
impl FocusableView for ImageView {
|
impl FocusableView for ImageView {
|
||||||
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
|
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
|
||||||
@ -242,13 +254,14 @@ impl ProjectItem for ImageView {
|
|||||||
|
|
||||||
pub fn init(cx: &mut AppContext) {
|
pub fn init(cx: &mut AppContext) {
|
||||||
workspace::register_project_item::<ImageView>(cx);
|
workspace::register_project_item::<ImageView>(cx);
|
||||||
workspace::register_deserializable_item::<ImageView>(cx)
|
workspace::register_serializable_item::<ImageView>(cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
mod persistence {
|
mod persistence {
|
||||||
|
use anyhow::Result;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use db::{define_connection, query, sqlez_macros::sql};
|
use db::{define_connection, query, sqlez::statement::Statement, sqlez_macros::sql};
|
||||||
use workspace::{ItemId, WorkspaceDb, WorkspaceId};
|
use workspace::{ItemId, WorkspaceDb, WorkspaceId};
|
||||||
|
|
||||||
define_connection! {
|
define_connection! {
|
||||||
@ -298,5 +311,29 @@ mod persistence {
|
|||||||
WHERE item_id = ? AND workspace_id = ?
|
WHERE item_id = ? AND workspace_id = ?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn delete_unloaded_items(
|
||||||
|
&self,
|
||||||
|
workspace: WorkspaceId,
|
||||||
|
alive_items: Vec<ItemId>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let placeholders = alive_items
|
||||||
|
.iter()
|
||||||
|
.map(|_| "?")
|
||||||
|
.collect::<Vec<&str>>()
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
|
let query = format!("DELETE FROM image_viewers WHERE workspace_id = ? AND item_id NOT IN ({placeholders})");
|
||||||
|
|
||||||
|
self.write(move |conn| {
|
||||||
|
let mut statement = Statement::prepare(conn, query)?;
|
||||||
|
let mut next_index = statement.bind(&workspace, 1)?;
|
||||||
|
for id in alive_items {
|
||||||
|
next_index = statement.bind(&id, next_index)?;
|
||||||
|
}
|
||||||
|
statement.exec()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,10 @@ pub struct ProjectSettings {
|
|||||||
/// Configuration for how direnv configuration should be loaded
|
/// Configuration for how direnv configuration should be loaded
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub load_direnv: DirenvSettings,
|
pub load_direnv: DirenvSettings,
|
||||||
|
|
||||||
|
/// Configuration for session-related features
|
||||||
|
#[serde(default)]
|
||||||
|
pub session: SessionSettings,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||||
@ -122,6 +126,25 @@ pub struct LspSettings {
|
|||||||
pub settings: Option<serde_json::Value>,
|
pub settings: Option<serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||||
|
pub struct SessionSettings {
|
||||||
|
/// Whether or not to restore unsaved buffers on restart.
|
||||||
|
///
|
||||||
|
/// If this is true, user won't be prompted whether to save/discard
|
||||||
|
/// dirty files when closing the application.
|
||||||
|
///
|
||||||
|
/// Default: true
|
||||||
|
pub restore_unsaved_buffers: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SessionSettings {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
restore_unsaved_buffers: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Settings for ProjectSettings {
|
impl Settings for ProjectSettings {
|
||||||
const KEY: Option<&'static str> = None;
|
const KEY: Option<&'static str> = None;
|
||||||
|
|
||||||
|
@ -38,5 +38,6 @@ workspace.workspace = true
|
|||||||
editor = { workspace = true, features = ["test-support"] }
|
editor = { workspace = true, features = ["test-support"] }
|
||||||
language = { workspace = true, features = ["test-support"] }
|
language = { workspace = true, features = ["test-support"] }
|
||||||
project = { workspace = true, features = ["test-support"] }
|
project = { workspace = true, features = ["test-support"] }
|
||||||
|
settings = { workspace = true, features = ["test-support"] }
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
workspace = { workspace = true, features = ["test-support"] }
|
workspace = { workspace = true, features = ["test-support"] }
|
||||||
|
@ -703,9 +703,10 @@ mod tests {
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use editor::Editor;
|
use editor::Editor;
|
||||||
use gpui::{TestAppContext, WindowHandle};
|
use gpui::{TestAppContext, UpdateGlobal, WindowHandle};
|
||||||
use project::Project;
|
use project::{project_settings::ProjectSettings, Project};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
use settings::SettingsStore;
|
||||||
use workspace::{open_paths, AppState, LocalPaths};
|
use workspace::{open_paths, AppState, LocalPaths};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@ -713,6 +714,15 @@ mod tests {
|
|||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_prompts_on_dirty_before_submit(cx: &mut TestAppContext) {
|
async fn test_prompts_on_dirty_before_submit(cx: &mut TestAppContext) {
|
||||||
let app_state = init_test(cx);
|
let app_state = init_test(cx);
|
||||||
|
|
||||||
|
cx.update(|cx| {
|
||||||
|
SettingsStore::update_global(cx, |store, cx| {
|
||||||
|
store.update_user_settings::<ProjectSettings>(cx, |settings| {
|
||||||
|
settings.session.restore_unsaved_buffers = false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
app_state
|
app_state
|
||||||
.fs
|
.fs
|
||||||
.as_fake()
|
.as_fake()
|
||||||
|
@ -16,7 +16,7 @@ use gpui::{
|
|||||||
EventEmitter, FocusHandle, FocusableView, FontStyle, Global, Hsla, InteractiveElement,
|
EventEmitter, FocusHandle, FocusableView, FontStyle, Global, Hsla, InteractiveElement,
|
||||||
IntoElement, Model, ModelContext, ParentElement, Point, Render, SharedString, Styled,
|
IntoElement, Model, ModelContext, ParentElement, Point, Render, SharedString, Styled,
|
||||||
Subscription, Task, TextStyle, UpdateGlobal, View, ViewContext, VisualContext, WeakModel,
|
Subscription, Task, TextStyle, UpdateGlobal, View, ViewContext, VisualContext, WeakModel,
|
||||||
WeakView, WhiteSpace, WindowContext,
|
WhiteSpace, WindowContext,
|
||||||
};
|
};
|
||||||
use menu::Confirm;
|
use menu::Confirm;
|
||||||
use project::{search::SearchQuery, search_history::SearchHistoryCursor, Project, ProjectPath};
|
use project::{search::SearchQuery, search_history::SearchHistoryCursor, Project, ProjectPath};
|
||||||
@ -37,7 +37,7 @@ use util::paths::PathMatcher;
|
|||||||
use workspace::{
|
use workspace::{
|
||||||
item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams},
|
item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams},
|
||||||
searchable::{Direction, SearchableItem, SearchableItemHandle},
|
searchable::{Direction, SearchableItem, SearchableItemHandle},
|
||||||
DeploySearch, ItemNavHistory, NewSearch, Pane, ToolbarItemEvent, ToolbarItemLocation,
|
DeploySearch, ItemNavHistory, NewSearch, ToolbarItemEvent, ToolbarItemLocation,
|
||||||
ToolbarItemView, Workspace, WorkspaceId,
|
ToolbarItemView, Workspace, WorkspaceId,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -506,20 +506,6 @@ impl Item for ProjectSearchView {
|
|||||||
fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
|
fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
|
||||||
self.results_editor.breadcrumbs(theme, cx)
|
self.results_editor.breadcrumbs(theme, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn serialized_item_kind() -> Option<&'static str> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deserialize(
|
|
||||||
_project: Model<Project>,
|
|
||||||
_workspace: WeakView<Workspace>,
|
|
||||||
_workspace_id: workspace::WorkspaceId,
|
|
||||||
_item_id: workspace::ItemId,
|
|
||||||
_cx: &mut ViewContext<Pane>,
|
|
||||||
) -> Task<anyhow::Result<View<Self>>> {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProjectSearchView {
|
impl ProjectSearchView {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
|
use anyhow::Result;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use db::{define_connection, query, sqlez_macros::sql};
|
use db::{define_connection, query, sqlez::statement::Statement, sqlez_macros::sql};
|
||||||
use workspace::{ItemId, WorkspaceDb, WorkspaceId};
|
use workspace::{ItemId, WorkspaceDb, WorkspaceId};
|
||||||
|
|
||||||
define_connection! {
|
define_connection! {
|
||||||
@ -68,4 +69,30 @@ impl TerminalDb {
|
|||||||
WHERE item_id = ? AND workspace_id = ?
|
WHERE item_id = ? AND workspace_id = ?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn delete_unloaded_items(
|
||||||
|
&self,
|
||||||
|
workspace: WorkspaceId,
|
||||||
|
alive_items: Vec<ItemId>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let placeholders = alive_items
|
||||||
|
.iter()
|
||||||
|
.map(|_| "?")
|
||||||
|
.collect::<Vec<&str>>()
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
|
let query = format!(
|
||||||
|
"DELETE FROM terminals WHERE workspace_id = ? AND item_id NOT IN ({placeholders})"
|
||||||
|
);
|
||||||
|
|
||||||
|
self.write(move |conn| {
|
||||||
|
let mut statement = Statement::prepare(conn, query)?;
|
||||||
|
let mut next_index = statement.bind(&workspace, 1)?;
|
||||||
|
for id in alive_items {
|
||||||
|
next_index = statement.bind(&id, next_index)?;
|
||||||
|
}
|
||||||
|
statement.exec()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,10 +26,10 @@ use ui::{
|
|||||||
use util::{ResultExt, TryFutureExt};
|
use util::{ResultExt, TryFutureExt};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
dock::{DockPosition, Panel, PanelEvent},
|
dock::{DockPosition, Panel, PanelEvent},
|
||||||
item::Item,
|
item::SerializableItem,
|
||||||
pane,
|
pane,
|
||||||
ui::IconName,
|
ui::IconName,
|
||||||
DraggedTab, NewTerminal, Pane, ToggleZoom, Workspace,
|
DraggedTab, ItemId, NewTerminal, Pane, ToggleZoom, Workspace,
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
@ -278,6 +278,7 @@ impl TerminalPanel {
|
|||||||
|
|
||||||
let pane = pane.downgrade();
|
let pane = pane.downgrade();
|
||||||
let items = futures::future::join_all(items).await;
|
let items = futures::future::join_all(items).await;
|
||||||
|
let mut alive_item_ids = Vec::new();
|
||||||
pane.update(&mut cx, |pane, cx| {
|
pane.update(&mut cx, |pane, cx| {
|
||||||
let active_item_id = serialized_panel
|
let active_item_id = serialized_panel
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@ -287,6 +288,7 @@ impl TerminalPanel {
|
|||||||
if let Some(item) = item.log_err() {
|
if let Some(item) = item.log_err() {
|
||||||
let item_id = item.entity_id().as_u64();
|
let item_id = item.entity_id().as_u64();
|
||||||
pane.add_item(Box::new(item), false, false, None, cx);
|
pane.add_item(Box::new(item), false, false, None, cx);
|
||||||
|
alive_item_ids.push(item_id as ItemId);
|
||||||
if Some(item_id) == active_item_id {
|
if Some(item_id) == active_item_id {
|
||||||
active_ix = Some(pane.items_len() - 1);
|
active_ix = Some(pane.items_len() - 1);
|
||||||
}
|
}
|
||||||
@ -298,6 +300,18 @@ impl TerminalPanel {
|
|||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
// Since panels/docks are loaded outside from the workspace, we cleanup here, instead of through the workspace.
|
||||||
|
if let Some(workspace) = workspace.upgrade() {
|
||||||
|
let cleanup_task = workspace.update(&mut cx, |workspace, cx| {
|
||||||
|
workspace
|
||||||
|
.database_id()
|
||||||
|
.map(|workspace_id| TerminalView::cleanup(workspace_id, alive_item_ids, cx))
|
||||||
|
})?;
|
||||||
|
if let Some(task) = cleanup_task {
|
||||||
|
task.await.log_err();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(panel)
|
Ok(panel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,9 +29,9 @@ use terminal_element::{is_blank, TerminalElement};
|
|||||||
use ui::{h_flex, prelude::*, ContextMenu, Icon, IconName, Label, Tooltip};
|
use ui::{h_flex, prelude::*, ContextMenu, Icon, IconName, Label, Tooltip};
|
||||||
use util::{paths::PathLikeWithPosition, ResultExt};
|
use util::{paths::PathLikeWithPosition, ResultExt};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
item::{BreadcrumbText, Item, ItemEvent, TabContentParams},
|
item::{BreadcrumbText, Item, ItemEvent, SerializableItem, TabContentParams},
|
||||||
notifications::NotifyResultExt,
|
notifications::NotifyResultExt,
|
||||||
register_deserializable_item,
|
register_serializable_item,
|
||||||
searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle},
|
searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle},
|
||||||
CloseActiveItem, NewCenterTerminal, OpenVisible, Pane, ToolbarItemLocation, Workspace,
|
CloseActiveItem, NewCenterTerminal, OpenVisible, Pane, ToolbarItemLocation, Workspace,
|
||||||
WorkspaceId,
|
WorkspaceId,
|
||||||
@ -73,7 +73,7 @@ pub fn init(cx: &mut AppContext) {
|
|||||||
terminal_panel::init(cx);
|
terminal_panel::init(cx);
|
||||||
terminal::init(cx);
|
terminal::init(cx);
|
||||||
|
|
||||||
register_deserializable_item::<TerminalView>(cx);
|
register_serializable_item::<TerminalView>(cx);
|
||||||
|
|
||||||
cx.observe_new_views(|workspace: &mut Workspace, _| {
|
cx.observe_new_views(|workspace: &mut Workspace, _| {
|
||||||
workspace.register_action(TerminalView::deploy);
|
workspace.register_action(TerminalView::deploy);
|
||||||
@ -612,22 +612,6 @@ fn subscribe_for_terminal_events(
|
|||||||
|
|
||||||
Event::TitleChanged => {
|
Event::TitleChanged => {
|
||||||
cx.emit(ItemEvent::UpdateTab);
|
cx.emit(ItemEvent::UpdateTab);
|
||||||
let terminal = this.terminal().read(cx);
|
|
||||||
if terminal.task().is_none() {
|
|
||||||
if let Some(cwd) = terminal.get_cwd() {
|
|
||||||
let item_id = cx.entity_id();
|
|
||||||
if let Some(workspace_id) = this.workspace_id {
|
|
||||||
cx.background_executor()
|
|
||||||
.spawn(async move {
|
|
||||||
TERMINAL_DB
|
|
||||||
.save_working_directory(item_id.as_u64(), workspace_id, cwd)
|
|
||||||
.await
|
|
||||||
.log_err();
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Event::NewNavigationTarget(maybe_navigation_target) => {
|
Event::NewNavigationTarget(maybe_navigation_target) => {
|
||||||
@ -1072,8 +1056,60 @@ impl Item for TerminalView {
|
|||||||
}])
|
}])
|
||||||
}
|
}
|
||||||
|
|
||||||
fn serialized_item_kind() -> Option<&'static str> {
|
fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
|
||||||
Some("Terminal")
|
if self.terminal().read(cx).task().is_none() {
|
||||||
|
if let Some((new_id, old_id)) = workspace.database_id().zip(self.workspace_id) {
|
||||||
|
cx.background_executor()
|
||||||
|
.spawn(TERMINAL_DB.update_workspace_id(new_id, old_id, cx.entity_id().as_u64()))
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
self.workspace_id = workspace.database_id();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
|
||||||
|
f(*event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SerializableItem for TerminalView {
|
||||||
|
fn serialized_item_kind() -> &'static str {
|
||||||
|
"Terminal"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cleanup(
|
||||||
|
workspace_id: WorkspaceId,
|
||||||
|
alive_items: Vec<workspace::ItemId>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> Task<gpui::Result<()>> {
|
||||||
|
cx.spawn(|_| TERMINAL_DB.delete_unloaded_items(workspace_id, alive_items))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialize(
|
||||||
|
&mut self,
|
||||||
|
_workspace: &mut Workspace,
|
||||||
|
item_id: workspace::ItemId,
|
||||||
|
_closing: bool,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Option<Task<gpui::Result<()>>> {
|
||||||
|
let terminal = self.terminal().read(cx);
|
||||||
|
if terminal.task().is_some() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((cwd, workspace_id)) = terminal.get_cwd().zip(self.workspace_id) {
|
||||||
|
Some(cx.background_executor().spawn(async move {
|
||||||
|
TERMINAL_DB
|
||||||
|
.save_working_directory(item_id, workspace_id, cwd)
|
||||||
|
.await
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_serialize(&self, event: &Self::Event) -> bool {
|
||||||
|
matches!(event, ItemEvent::UpdateTab)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deserialize(
|
fn deserialize(
|
||||||
@ -1116,21 +1152,6 @@ impl Item for TerminalView {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
|
|
||||||
if self.terminal().read(cx).task().is_none() {
|
|
||||||
if let Some((new_id, old_id)) = workspace.database_id().zip(self.workspace_id) {
|
|
||||||
cx.background_executor()
|
|
||||||
.spawn(TERMINAL_DB.update_workspace_id(new_id, old_id, cx.entity_id().as_u64()))
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
self.workspace_id = workspace.database_id();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
|
|
||||||
f(*event)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SearchableItem for TerminalView {
|
impl SearchableItem for TerminalView {
|
||||||
|
@ -3,8 +3,8 @@ use crate::{
|
|||||||
persistence::model::ItemId,
|
persistence::model::ItemId,
|
||||||
searchable::SearchableItemHandle,
|
searchable::SearchableItemHandle,
|
||||||
workspace_settings::{AutosaveSetting, WorkspaceSettings},
|
workspace_settings::{AutosaveSetting, WorkspaceSettings},
|
||||||
DelayedDebouncedEditAction, FollowableViewRegistry, ItemNavHistory, ToolbarItemLocation,
|
DelayedDebouncedEditAction, FollowableViewRegistry, ItemNavHistory, SerializableItemRegistry,
|
||||||
ViewId, Workspace, WorkspaceId,
|
ToolbarItemLocation, ViewId, Workspace, WorkspaceId,
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use client::{
|
use client::{
|
||||||
@ -32,6 +32,7 @@ use std::{
|
|||||||
};
|
};
|
||||||
use theme::Theme;
|
use theme::Theme;
|
||||||
use ui::Element as _;
|
use ui::Element as _;
|
||||||
|
use util::ResultExt;
|
||||||
|
|
||||||
pub const LEADER_UPDATE_THROTTLE: Duration = Duration::from_millis(200);
|
pub const LEADER_UPDATE_THROTTLE: Duration = Duration::from_millis(200);
|
||||||
|
|
||||||
@ -245,9 +246,23 @@ pub trait Item: FocusableView + EventEmitter<Self::Event> {
|
|||||||
|
|
||||||
fn added_to_workspace(&mut self, _workspace: &mut Workspace, _cx: &mut ViewContext<Self>) {}
|
fn added_to_workspace(&mut self, _workspace: &mut Workspace, _cx: &mut ViewContext<Self>) {}
|
||||||
|
|
||||||
fn serialized_item_kind() -> Option<&'static str> {
|
fn show_toolbar(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pixel_position_of_cursor(&self, _: &AppContext) -> Option<Point<Pixels>> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait SerializableItem: Item {
|
||||||
|
fn serialized_item_kind() -> &'static str;
|
||||||
|
|
||||||
|
fn cleanup(
|
||||||
|
workspace_id: WorkspaceId,
|
||||||
|
alive_items: Vec<ItemId>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> Task<Result<()>>;
|
||||||
|
|
||||||
fn deserialize(
|
fn deserialize(
|
||||||
_project: Model<Project>,
|
_project: Model<Project>,
|
||||||
@ -255,16 +270,53 @@ pub trait Item: FocusableView + EventEmitter<Self::Event> {
|
|||||||
_workspace_id: WorkspaceId,
|
_workspace_id: WorkspaceId,
|
||||||
_item_id: ItemId,
|
_item_id: ItemId,
|
||||||
_cx: &mut ViewContext<Pane>,
|
_cx: &mut ViewContext<Pane>,
|
||||||
) -> Task<Result<View<Self>>> {
|
) -> Task<Result<View<Self>>>;
|
||||||
unimplemented!(
|
|
||||||
"deserialize() must be implemented if serialized_item_kind() returns Some(_)"
|
fn serialize(
|
||||||
)
|
&mut self,
|
||||||
|
workspace: &mut Workspace,
|
||||||
|
item_id: ItemId,
|
||||||
|
closing: bool,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Option<Task<Result<()>>>;
|
||||||
|
|
||||||
|
fn should_serialize(&self, event: &Self::Event) -> bool;
|
||||||
}
|
}
|
||||||
fn show_toolbar(&self) -> bool {
|
|
||||||
true
|
pub trait SerializableItemHandle: ItemHandle {
|
||||||
|
fn serialized_item_kind(&self) -> &'static str;
|
||||||
|
fn serialize(
|
||||||
|
&self,
|
||||||
|
workspace: &mut Workspace,
|
||||||
|
closing: bool,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> Option<Task<Result<()>>>;
|
||||||
|
fn should_serialize(&self, event: &dyn Any, cx: &AppContext) -> bool;
|
||||||
}
|
}
|
||||||
fn pixel_position_of_cursor(&self, _: &AppContext) -> Option<Point<Pixels>> {
|
|
||||||
None
|
impl<T> SerializableItemHandle for View<T>
|
||||||
|
where
|
||||||
|
T: SerializableItem,
|
||||||
|
{
|
||||||
|
fn serialized_item_kind(&self) -> &'static str {
|
||||||
|
T::serialized_item_kind()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialize(
|
||||||
|
&self,
|
||||||
|
workspace: &mut Workspace,
|
||||||
|
closing: bool,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> Option<Task<Result<()>>> {
|
||||||
|
self.update(cx, |this, cx| {
|
||||||
|
this.serialize(workspace, cx.entity_id().as_u64(), closing, cx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_serialize(&self, event: &dyn Any, cx: &AppContext) -> bool {
|
||||||
|
event
|
||||||
|
.downcast_ref::<T::Event>()
|
||||||
|
.map_or(false, |event| self.read(cx).should_serialize(event))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -324,6 +376,10 @@ pub trait ItemHandle: 'static + Send {
|
|||||||
fn reload(&self, project: Model<Project>, cx: &mut WindowContext) -> Task<Result<()>>;
|
fn reload(&self, project: Model<Project>, cx: &mut WindowContext) -> Task<Result<()>>;
|
||||||
fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyView>;
|
fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyView>;
|
||||||
fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>>;
|
fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>>;
|
||||||
|
fn to_serializable_item_handle(
|
||||||
|
&self,
|
||||||
|
cx: &AppContext,
|
||||||
|
) -> Option<Box<dyn SerializableItemHandle>>;
|
||||||
fn on_release(
|
fn on_release(
|
||||||
&self,
|
&self,
|
||||||
cx: &mut AppContext,
|
cx: &mut AppContext,
|
||||||
@ -332,7 +388,6 @@ pub trait ItemHandle: 'static + Send {
|
|||||||
fn to_searchable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>>;
|
fn to_searchable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn SearchableItemHandle>>;
|
||||||
fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation;
|
fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation;
|
||||||
fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>>;
|
fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>>;
|
||||||
fn serialized_item_kind(&self) -> Option<&'static str>;
|
|
||||||
fn show_toolbar(&self, cx: &AppContext) -> bool;
|
fn show_toolbar(&self, cx: &AppContext) -> bool;
|
||||||
fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Point<Pixels>>;
|
fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option<Point<Pixels>>;
|
||||||
fn downgrade_item(&self) -> Box<dyn WeakItemHandle>;
|
fn downgrade_item(&self) -> Box<dyn WeakItemHandle>;
|
||||||
@ -477,6 +532,12 @@ impl<T: Item> ItemHandle for View<T> {
|
|||||||
this.added_to_workspace(workspace, cx);
|
this.added_to_workspace(workspace, cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if let Some(serializable_item) = self.to_serializable_item_handle(cx) {
|
||||||
|
workspace
|
||||||
|
.enqueue_item_serialization(serializable_item)
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
|
||||||
if workspace
|
if workspace
|
||||||
.panes_by_item
|
.panes_by_item
|
||||||
.insert(self.item_id(), pane.downgrade())
|
.insert(self.item_id(), pane.downgrade())
|
||||||
@ -554,6 +615,12 @@ impl<T: Item> ItemHandle for View<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(item) = item.to_serializable_item_handle(cx) {
|
||||||
|
if item.should_serialize(event, cx) {
|
||||||
|
workspace.enqueue_item_serialization(item).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
T::to_item_events(event, |event| match event {
|
T::to_item_events(event, |event| match event {
|
||||||
ItemEvent::CloseItem => {
|
ItemEvent::CloseItem => {
|
||||||
pane.update(cx, |pane, cx| {
|
pane.update(cx, |pane, cx| {
|
||||||
@ -694,10 +761,6 @@ impl<T: Item> ItemHandle for View<T> {
|
|||||||
self.read(cx).breadcrumbs(theme, cx)
|
self.read(cx).breadcrumbs(theme, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn serialized_item_kind(&self) -> Option<&'static str> {
|
|
||||||
T::serialized_item_kind()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn show_toolbar(&self, cx: &AppContext) -> bool {
|
fn show_toolbar(&self, cx: &AppContext) -> bool {
|
||||||
self.read(cx).show_toolbar()
|
self.read(cx).show_toolbar()
|
||||||
}
|
}
|
||||||
@ -709,6 +772,13 @@ impl<T: Item> ItemHandle for View<T> {
|
|||||||
fn downgrade_item(&self) -> Box<dyn WeakItemHandle> {
|
fn downgrade_item(&self) -> Box<dyn WeakItemHandle> {
|
||||||
Box::new(self.downgrade())
|
Box::new(self.downgrade())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn to_serializable_item_handle(
|
||||||
|
&self,
|
||||||
|
cx: &AppContext,
|
||||||
|
) -> Option<Box<dyn SerializableItemHandle>> {
|
||||||
|
SerializableItemRegistry::view_to_serializable_item_handle(self.to_any(), cx)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Box<dyn ItemHandle>> for AnyView {
|
impl From<Box<dyn ItemHandle>> for AnyView {
|
||||||
@ -880,7 +950,7 @@ impl<T: FollowableItem> WeakFollowableItemHandle for WeakView<T> {
|
|||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
pub mod test {
|
pub mod test {
|
||||||
use super::{Item, ItemEvent, TabContentParams};
|
use super::{Item, ItemEvent, SerializableItem, TabContentParams};
|
||||||
use crate::{ItemId, ItemNavHistory, Pane, Workspace, WorkspaceId};
|
use crate::{ItemId, ItemNavHistory, Pane, Workspace, WorkspaceId};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, AppContext, Context as _, EntityId, EventEmitter, FocusableView,
|
AnyElement, AppContext, Context as _, EntityId, EventEmitter, FocusableView,
|
||||||
@ -909,6 +979,7 @@ pub mod test {
|
|||||||
pub nav_history: Option<ItemNavHistory>,
|
pub nav_history: Option<ItemNavHistory>,
|
||||||
pub tab_descriptions: Option<Vec<&'static str>>,
|
pub tab_descriptions: Option<Vec<&'static str>>,
|
||||||
pub tab_detail: Cell<Option<usize>>,
|
pub tab_detail: Cell<Option<usize>>,
|
||||||
|
serialize: Option<Box<dyn Fn() -> Option<Task<anyhow::Result<()>>>>>,
|
||||||
focus_handle: gpui::FocusHandle,
|
focus_handle: gpui::FocusHandle,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -972,6 +1043,7 @@ pub mod test {
|
|||||||
tab_detail: Default::default(),
|
tab_detail: Default::default(),
|
||||||
workspace_id: Default::default(),
|
workspace_id: Default::default(),
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
|
serialize: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1007,6 +1079,14 @@ pub mod test {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_serialize(
|
||||||
|
mut self,
|
||||||
|
serialize: impl Fn() -> Option<Task<anyhow::Result<()>>> + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.serialize = Some(Box::new(serialize));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_state(&mut self, state: String, cx: &mut ViewContext<Self>) {
|
pub fn set_state(&mut self, state: String, cx: &mut ViewContext<Self>) {
|
||||||
self.push_to_nav_history(cx);
|
self.push_to_nav_history(cx);
|
||||||
self.state = state;
|
self.state = state;
|
||||||
@ -1115,6 +1195,7 @@ pub mod test {
|
|||||||
tab_detail: Default::default(),
|
tab_detail: Default::default(),
|
||||||
workspace_id: self.workspace_id,
|
workspace_id: self.workspace_id,
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
|
serialize: None,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1165,9 +1246,11 @@ pub mod test {
|
|||||||
self.is_dirty = false;
|
self.is_dirty = false;
|
||||||
Task::ready(Ok(()))
|
Task::ready(Ok(()))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn serialized_item_kind() -> Option<&'static str> {
|
impl SerializableItem for TestItem {
|
||||||
Some("TestItem")
|
fn serialized_item_kind() -> &'static str {
|
||||||
|
"TestItem"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deserialize(
|
fn deserialize(
|
||||||
@ -1178,7 +1261,35 @@ pub mod test {
|
|||||||
cx: &mut ViewContext<Pane>,
|
cx: &mut ViewContext<Pane>,
|
||||||
) -> Task<anyhow::Result<View<Self>>> {
|
) -> Task<anyhow::Result<View<Self>>> {
|
||||||
let view = cx.new_view(|cx| Self::new_deserialized(workspace_id, cx));
|
let view = cx.new_view(|cx| Self::new_deserialized(workspace_id, cx));
|
||||||
Task::Ready(Some(anyhow::Ok(view)))
|
Task::ready(Ok(view))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cleanup(
|
||||||
|
_workspace_id: WorkspaceId,
|
||||||
|
_alive_items: Vec<ItemId>,
|
||||||
|
_cx: &mut ui::WindowContext,
|
||||||
|
) -> Task<anyhow::Result<()>> {
|
||||||
|
Task::ready(Ok(()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialize(
|
||||||
|
&mut self,
|
||||||
|
_workspace: &mut Workspace,
|
||||||
|
_item_id: ItemId,
|
||||||
|
_closing: bool,
|
||||||
|
_cx: &mut ViewContext<Self>,
|
||||||
|
) -> Option<Task<anyhow::Result<()>>> {
|
||||||
|
if let Some(serialize) = self.serialize.take() {
|
||||||
|
let result = serialize();
|
||||||
|
self.serialize = Some(serialize);
|
||||||
|
result
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_serialize(&self, _event: &Self::Event) -> bool {
|
||||||
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1478,6 +1478,7 @@ impl Pane {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
use super::{SerializedAxis, SerializedWindowBounds};
|
use super::{SerializedAxis, SerializedWindowBounds};
|
||||||
use crate::{item::ItemHandle, ItemDeserializers, Member, Pane, PaneAxis, Workspace, WorkspaceId};
|
use crate::{
|
||||||
|
item::ItemHandle, Member, Pane, PaneAxis, SerializableItemRegistry, Workspace, WorkspaceId,
|
||||||
|
};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use async_recursion::async_recursion;
|
use async_recursion::async_recursion;
|
||||||
use client::DevServerProjectId;
|
use client::DevServerProjectId;
|
||||||
@ -7,7 +9,7 @@ use db::sqlez::{
|
|||||||
bindable::{Bind, Column, StaticColumnCount},
|
bindable::{Bind, Column, StaticColumnCount},
|
||||||
statement::Statement,
|
statement::Statement,
|
||||||
};
|
};
|
||||||
use gpui::{AsyncWindowContext, Model, Task, View, WeakView};
|
use gpui::{AsyncWindowContext, Model, View, WeakView};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
@ -339,14 +341,14 @@ impl SerializedPane {
|
|||||||
for (index, item) in self.children.iter().enumerate() {
|
for (index, item) in self.children.iter().enumerate() {
|
||||||
let project = project.clone();
|
let project = project.clone();
|
||||||
item_tasks.push(pane.update(cx, |_, cx| {
|
item_tasks.push(pane.update(cx, |_, cx| {
|
||||||
if let Some(deserializer) = cx.global::<ItemDeserializers>().get(&item.kind) {
|
SerializableItemRegistry::deserialize(
|
||||||
deserializer(project, workspace.clone(), workspace_id, item.item_id, cx)
|
&item.kind,
|
||||||
} else {
|
project,
|
||||||
Task::ready(Err(anyhow::anyhow!(
|
workspace.clone(),
|
||||||
"Deserializer does not exist for item kind: {}",
|
workspace_id,
|
||||||
item.kind
|
item.item_id,
|
||||||
)))
|
cx,
|
||||||
}
|
)
|
||||||
})?);
|
})?);
|
||||||
if item.active {
|
if item.active {
|
||||||
active_item_index = Some(index);
|
active_item_index = Some(index);
|
||||||
|
@ -22,7 +22,10 @@ use collections::{hash_map, HashMap, HashSet};
|
|||||||
use derive_more::{Deref, DerefMut};
|
use derive_more::{Deref, DerefMut};
|
||||||
use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle};
|
use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle};
|
||||||
use futures::{
|
use futures::{
|
||||||
channel::{mpsc, oneshot},
|
channel::{
|
||||||
|
mpsc::{self, UnboundedReceiver, UnboundedSender},
|
||||||
|
oneshot,
|
||||||
|
},
|
||||||
future::try_join_all,
|
future::try_join_all,
|
||||||
Future, FutureExt, StreamExt,
|
Future, FutureExt, StreamExt,
|
||||||
};
|
};
|
||||||
@ -37,7 +40,7 @@ use gpui::{
|
|||||||
};
|
};
|
||||||
use item::{
|
use item::{
|
||||||
FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
|
FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
|
||||||
ProjectItem,
|
ProjectItem, SerializableItem, SerializableItemHandle,
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use language::{LanguageRegistry, Rope};
|
use language::{LanguageRegistry, Rope};
|
||||||
@ -85,7 +88,7 @@ use ui::{
|
|||||||
IntoElement, ParentElement as _, Pixels, SharedString, Styled as _, ViewContext,
|
IntoElement, ParentElement as _, Pixels, SharedString, Styled as _, ViewContext,
|
||||||
VisualContext as _, WindowContext,
|
VisualContext as _, WindowContext,
|
||||||
};
|
};
|
||||||
use util::{maybe, ResultExt};
|
use util::{maybe, ResultExt, TryFutureExt};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
pub use workspace_settings::{
|
pub use workspace_settings::{
|
||||||
AutosaveSetting, RestoreOnStartupBehaviour, TabBarSettings, WorkspaceSettings,
|
AutosaveSetting, RestoreOnStartupBehaviour, TabBarSettings, WorkspaceSettings,
|
||||||
@ -280,6 +283,12 @@ impl Column for WorkspaceId {
|
|||||||
.with_context(|| format!("Failed to read WorkspaceId at index {start_index}"))
|
.with_context(|| format!("Failed to read WorkspaceId at index {start_index}"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl Into<i64> for WorkspaceId {
|
||||||
|
fn into(self) -> i64 {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn init_settings(cx: &mut AppContext) {
|
pub fn init_settings(cx: &mut AppContext) {
|
||||||
WorkspaceSettings::register(cx);
|
WorkspaceSettings::register(cx);
|
||||||
ItemSettings::register(cx);
|
ItemSettings::register(cx);
|
||||||
@ -427,34 +436,96 @@ impl FollowableViewRegistry {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Deref, DerefMut)]
|
#[derive(Copy, Clone)]
|
||||||
struct ItemDeserializers(
|
struct SerializableItemDescriptor {
|
||||||
HashMap<
|
deserialize: fn(
|
||||||
Arc<str>,
|
|
||||||
fn(
|
|
||||||
Model<Project>,
|
Model<Project>,
|
||||||
WeakView<Workspace>,
|
WeakView<Workspace>,
|
||||||
WorkspaceId,
|
WorkspaceId,
|
||||||
ItemId,
|
ItemId,
|
||||||
&mut ViewContext<Pane>,
|
&mut ViewContext<Pane>,
|
||||||
) -> Task<Result<Box<dyn ItemHandle>>>,
|
) -> Task<Result<Box<dyn ItemHandle>>>,
|
||||||
>,
|
cleanup: fn(WorkspaceId, Vec<ItemId>, &mut WindowContext) -> Task<Result<()>>,
|
||||||
);
|
view_to_serializable_item: fn(AnyView) -> Box<dyn SerializableItemHandle>,
|
||||||
|
}
|
||||||
|
|
||||||
impl Global for ItemDeserializers {}
|
#[derive(Default)]
|
||||||
|
struct SerializableItemRegistry {
|
||||||
|
descriptors_by_kind: HashMap<Arc<str>, SerializableItemDescriptor>,
|
||||||
|
descriptors_by_type: HashMap<TypeId, SerializableItemDescriptor>,
|
||||||
|
}
|
||||||
|
|
||||||
pub fn register_deserializable_item<I: Item>(cx: &mut AppContext) {
|
impl Global for SerializableItemRegistry {}
|
||||||
if let Some(serialized_item_kind) = I::serialized_item_kind() {
|
|
||||||
let deserializers = cx.default_global::<ItemDeserializers>();
|
impl SerializableItemRegistry {
|
||||||
deserializers.insert(
|
fn deserialize(
|
||||||
Arc::from(serialized_item_kind),
|
item_kind: &str,
|
||||||
|project, workspace, workspace_id, item_id, cx| {
|
project: Model<Project>,
|
||||||
|
workspace: WeakView<Workspace>,
|
||||||
|
workspace_id: WorkspaceId,
|
||||||
|
item_item: ItemId,
|
||||||
|
cx: &mut ViewContext<Pane>,
|
||||||
|
) -> Task<Result<Box<dyn ItemHandle>>> {
|
||||||
|
let Some(descriptor) = Self::descriptor(item_kind, cx) else {
|
||||||
|
return Task::ready(Err(anyhow!(
|
||||||
|
"cannot deserialize {}, descriptor not found",
|
||||||
|
item_kind
|
||||||
|
)));
|
||||||
|
};
|
||||||
|
|
||||||
|
(descriptor.deserialize)(project, workspace, workspace_id, item_item, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cleanup(
|
||||||
|
item_kind: &str,
|
||||||
|
workspace_id: WorkspaceId,
|
||||||
|
loaded_items: Vec<ItemId>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> Task<Result<()>> {
|
||||||
|
let Some(descriptor) = Self::descriptor(item_kind, cx) else {
|
||||||
|
return Task::ready(Err(anyhow!(
|
||||||
|
"cannot cleanup {}, descriptor not found",
|
||||||
|
item_kind
|
||||||
|
)));
|
||||||
|
};
|
||||||
|
|
||||||
|
(descriptor.cleanup)(workspace_id, loaded_items, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_to_serializable_item_handle(
|
||||||
|
view: AnyView,
|
||||||
|
cx: &AppContext,
|
||||||
|
) -> Option<Box<dyn SerializableItemHandle>> {
|
||||||
|
let this = cx.try_global::<Self>()?;
|
||||||
|
let descriptor = this.descriptors_by_type.get(&view.entity_type())?;
|
||||||
|
Some((descriptor.view_to_serializable_item)(view))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn descriptor(item_kind: &str, cx: &AppContext) -> Option<SerializableItemDescriptor> {
|
||||||
|
let this = cx.try_global::<Self>()?;
|
||||||
|
this.descriptors_by_kind.get(item_kind).copied()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register_serializable_item<I: SerializableItem>(cx: &mut AppContext) {
|
||||||
|
let serialized_item_kind = I::serialized_item_kind();
|
||||||
|
|
||||||
|
let registry = cx.default_global::<SerializableItemRegistry>();
|
||||||
|
let descriptor = SerializableItemDescriptor {
|
||||||
|
deserialize: |project, workspace, workspace_id, item_id, cx| {
|
||||||
let task = I::deserialize(project, workspace, workspace_id, item_id, cx);
|
let task = I::deserialize(project, workspace, workspace_id, item_id, cx);
|
||||||
cx.foreground_executor()
|
cx.foreground_executor()
|
||||||
.spawn(async { Ok(Box::new(task.await?) as Box<_>) })
|
.spawn(async { Ok(Box::new(task.await?) as Box<_>) })
|
||||||
},
|
},
|
||||||
);
|
cleanup: |workspace_id, loaded_items, cx| I::cleanup(workspace_id, loaded_items, cx),
|
||||||
}
|
view_to_serializable_item: |view| Box::new(view.downcast::<I>().unwrap()),
|
||||||
|
};
|
||||||
|
registry
|
||||||
|
.descriptors_by_kind
|
||||||
|
.insert(Arc::from(serialized_item_kind), descriptor);
|
||||||
|
registry
|
||||||
|
.descriptors_by_type
|
||||||
|
.insert(TypeId::of::<I>(), descriptor);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
@ -657,6 +728,8 @@ pub struct Workspace {
|
|||||||
on_prompt_for_open_path: Option<PromptForOpenPath>,
|
on_prompt_for_open_path: Option<PromptForOpenPath>,
|
||||||
render_disconnected_overlay:
|
render_disconnected_overlay:
|
||||||
Option<Box<dyn Fn(&mut Self, &mut ViewContext<Self>) -> AnyElement>>,
|
Option<Box<dyn Fn(&mut Self, &mut ViewContext<Self>) -> AnyElement>>,
|
||||||
|
serializable_items_tx: UnboundedSender<Box<dyn SerializableItemHandle>>,
|
||||||
|
_items_serializer: Task<Result<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<Event> for Workspace {}
|
impl EventEmitter<Event> for Workspace {}
|
||||||
@ -842,6 +915,12 @@ impl Workspace {
|
|||||||
active_call = Some((call, subscriptions));
|
active_call = Some((call, subscriptions));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let (serializable_items_tx, serializable_items_rx) =
|
||||||
|
mpsc::unbounded::<Box<dyn SerializableItemHandle>>();
|
||||||
|
let _items_serializer = cx.spawn(|this, mut cx| async move {
|
||||||
|
Self::serialize_items(&this, serializable_items_rx, &mut cx).await
|
||||||
|
});
|
||||||
|
|
||||||
let subscriptions = vec![
|
let subscriptions = vec![
|
||||||
cx.observe_window_activation(Self::on_window_activation_changed),
|
cx.observe_window_activation(Self::on_window_activation_changed),
|
||||||
cx.observe_window_bounds(move |this, cx| {
|
cx.observe_window_bounds(move |this, cx| {
|
||||||
@ -942,6 +1021,8 @@ impl Workspace {
|
|||||||
on_prompt_for_new_path: None,
|
on_prompt_for_new_path: None,
|
||||||
on_prompt_for_open_path: None,
|
on_prompt_for_open_path: None,
|
||||||
render_disconnected_overlay: None,
|
render_disconnected_overlay: None,
|
||||||
|
serializable_items_tx,
|
||||||
|
_items_serializer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1649,12 +1730,31 @@ impl Workspace {
|
|||||||
|
|
||||||
let project = self.project.clone();
|
let project = self.project.clone();
|
||||||
cx.spawn(|workspace, mut cx| async move {
|
cx.spawn(|workspace, mut cx| async move {
|
||||||
// Override save mode and display "Save all files" prompt
|
let dirty_items = if save_intent == SaveIntent::Close && dirty_items.len() > 0 {
|
||||||
if save_intent == SaveIntent::Close && dirty_items.len() > 1 {
|
let (serialize_tasks, remaining_dirty_items) =
|
||||||
|
workspace.update(&mut cx, |workspace, cx| {
|
||||||
|
let mut remaining_dirty_items = Vec::new();
|
||||||
|
let mut serialize_tasks = Vec::new();
|
||||||
|
for (pane, item) in dirty_items {
|
||||||
|
if let Some(task) = item
|
||||||
|
.to_serializable_item_handle(cx)
|
||||||
|
.and_then(|handle| handle.serialize(workspace, true, cx))
|
||||||
|
{
|
||||||
|
serialize_tasks.push(task);
|
||||||
|
} else {
|
||||||
|
remaining_dirty_items.push((pane, item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(serialize_tasks, remaining_dirty_items)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
futures::future::try_join_all(serialize_tasks).await?;
|
||||||
|
|
||||||
|
if remaining_dirty_items.len() > 1 {
|
||||||
let answer = workspace.update(&mut cx, |_, cx| {
|
let answer = workspace.update(&mut cx, |_, cx| {
|
||||||
let (prompt, detail) = Pane::file_names_for_prompt(
|
let (prompt, detail) = Pane::file_names_for_prompt(
|
||||||
&mut dirty_items.iter().map(|(_, handle)| handle),
|
&mut remaining_dirty_items.iter().map(|(_, handle)| handle),
|
||||||
dirty_items.len(),
|
remaining_dirty_items.len(),
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
cx.prompt(
|
cx.prompt(
|
||||||
@ -1670,6 +1770,12 @@ impl Workspace {
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
remaining_dirty_items
|
||||||
|
} else {
|
||||||
|
dirty_items
|
||||||
|
};
|
||||||
|
|
||||||
for (pane, item) in dirty_items {
|
for (pane, item) in dirty_items {
|
||||||
let (singleton, project_entry_ids) =
|
let (singleton, project_entry_ids) =
|
||||||
cx.update(|cx| (item.is_singleton(cx), item.project_entry_ids(cx)))?;
|
cx.update(|cx| (item.is_singleton(cx), item.project_entry_ids(cx)))?;
|
||||||
@ -3743,12 +3849,14 @@ impl Workspace {
|
|||||||
let active_item_id = pane.active_item().map(|item| item.item_id());
|
let active_item_id = pane.active_item().map(|item| item.item_id());
|
||||||
(
|
(
|
||||||
pane.items()
|
pane.items()
|
||||||
.filter_map(|item_handle| {
|
.filter_map(|handle| {
|
||||||
|
let handle = handle.to_serializable_item_handle(cx)?;
|
||||||
|
|
||||||
Some(SerializedItem {
|
Some(SerializedItem {
|
||||||
kind: Arc::from(item_handle.serialized_item_kind()?),
|
kind: Arc::from(handle.serialized_item_kind()),
|
||||||
item_id: item_handle.item_id().as_u64(),
|
item_id: handle.item_id().as_u64(),
|
||||||
active: Some(item_handle.item_id()) == active_item_id,
|
active: Some(handle.item_id()) == active_item_id,
|
||||||
preview: pane.is_active_preview_item(item_handle.item_id()),
|
preview: pane.is_active_preview_item(handle.item_id()),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
@ -3885,6 +3993,52 @@ impl Workspace {
|
|||||||
Task::ready(())
|
Task::ready(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn serialize_items(
|
||||||
|
this: &WeakView<Self>,
|
||||||
|
items_rx: UnboundedReceiver<Box<dyn SerializableItemHandle>>,
|
||||||
|
cx: &mut AsyncWindowContext,
|
||||||
|
) -> Result<()> {
|
||||||
|
const CHUNK_SIZE: usize = 200;
|
||||||
|
const THROTTLE_TIME: Duration = Duration::from_millis(200);
|
||||||
|
|
||||||
|
let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE);
|
||||||
|
|
||||||
|
while let Some(items_received) = serializable_items.next().await {
|
||||||
|
let unique_items =
|
||||||
|
items_received
|
||||||
|
.into_iter()
|
||||||
|
.fold(HashMap::default(), |mut acc, item| {
|
||||||
|
acc.entry(item.item_id()).or_insert(item);
|
||||||
|
acc
|
||||||
|
});
|
||||||
|
|
||||||
|
// We use into_iter() here so that the references to the items are moved into
|
||||||
|
// the tasks and not kept alive while we're sleeping.
|
||||||
|
for (_, item) in unique_items.into_iter() {
|
||||||
|
if let Ok(Some(task)) =
|
||||||
|
this.update(cx, |workspace, cx| item.serialize(workspace, false, cx))
|
||||||
|
{
|
||||||
|
cx.background_executor()
|
||||||
|
.spawn(async move { task.await.log_err() })
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.background_executor().timer(THROTTLE_TIME).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn enqueue_item_serialization(
|
||||||
|
&mut self,
|
||||||
|
item: Box<dyn SerializableItemHandle>,
|
||||||
|
) -> Result<()> {
|
||||||
|
self.serializable_items_tx
|
||||||
|
.unbounded_send(item)
|
||||||
|
.map_err(|err| anyhow!("failed to send serializable item over channel: {}", err))
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn load_workspace(
|
pub(crate) fn load_workspace(
|
||||||
serialized_workspace: SerializedWorkspace,
|
serialized_workspace: SerializedWorkspace,
|
||||||
paths_to_open: Vec<Option<ProjectPath>>,
|
paths_to_open: Vec<Option<ProjectPath>>,
|
||||||
@ -3911,16 +4065,23 @@ impl Workspace {
|
|||||||
center_group = Some((group, active_pane))
|
center_group = Some((group, active_pane))
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut items_by_project_path = cx.update(|cx| {
|
let mut items_by_project_path = HashMap::default();
|
||||||
center_items
|
let mut item_ids_by_kind = HashMap::default();
|
||||||
.unwrap_or_default()
|
let mut all_deserialized_items = Vec::default();
|
||||||
.into_iter()
|
cx.update(|cx| {
|
||||||
.filter_map(|item| {
|
for item in center_items.unwrap_or_default().into_iter().flatten() {
|
||||||
let item = item?;
|
if let Some(serializable_item_handle) = item.to_serializable_item_handle(cx) {
|
||||||
let project_path = item.project_path(cx)?;
|
item_ids_by_kind
|
||||||
Some((project_path, item))
|
.entry(serializable_item_handle.serialized_item_kind())
|
||||||
})
|
.or_insert(Vec::new())
|
||||||
.collect::<HashMap<_, _>>()
|
.push(item.item_id().as_u64() as ItemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(project_path) = item.project_path(cx) {
|
||||||
|
items_by_project_path.insert(project_path, item.clone());
|
||||||
|
}
|
||||||
|
all_deserialized_items.push(item);
|
||||||
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let opened_items = paths_to_open
|
let opened_items = paths_to_open
|
||||||
@ -3965,10 +4126,35 @@ impl Workspace {
|
|||||||
cx.notify();
|
cx.notify();
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Serialize ourself to make sure our timestamps and any pane / item changes are replicated
|
// Clean up all the items that have _not_ been loaded. Our ItemIds aren't stable. That means
|
||||||
|
// after loading the items, we might have different items and in order to avoid
|
||||||
|
// the database filling up, we delete items that haven't been loaded now.
|
||||||
|
//
|
||||||
|
// The items that have been loaded, have been saved after they've been added to the workspace.
|
||||||
|
let clean_up_tasks = workspace.update(&mut cx, |_, cx| {
|
||||||
|
item_ids_by_kind
|
||||||
|
.into_iter()
|
||||||
|
.map(|(item_kind, loaded_items)| {
|
||||||
|
SerializableItemRegistry::cleanup(
|
||||||
|
item_kind,
|
||||||
|
serialized_workspace.id,
|
||||||
|
loaded_items,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.log_err()
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
futures::future::join_all(clean_up_tasks).await;
|
||||||
|
|
||||||
workspace
|
workspace
|
||||||
.update(&mut cx, |workspace, cx| {
|
.update(&mut cx, |workspace, cx| {
|
||||||
|
// Serialize ourself to make sure our timestamps and any pane / item changes are replicated
|
||||||
workspace.serialize_workspace_internal(cx).detach();
|
workspace.serialize_workspace_internal(cx).detach();
|
||||||
|
|
||||||
|
// Ensure that we mark the window as edited if we did load dirty items
|
||||||
|
workspace.update_window_edited(cx);
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
@ -5557,6 +5743,41 @@ mod tests {
|
|||||||
assert!(!task.await.unwrap());
|
assert!(!task.await.unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
|
||||||
|
// Register TestItem as a serializable item
|
||||||
|
cx.update(|cx| {
|
||||||
|
register_serializable_item::<TestItem>(cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
fs.insert_tree("/root", json!({ "one": "" })).await;
|
||||||
|
|
||||||
|
let project = Project::test(fs, ["root".as_ref()], cx).await;
|
||||||
|
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
|
||||||
|
|
||||||
|
// When there are dirty untitled items, but they can serialize, then there is no prompt.
|
||||||
|
let item1 = cx.new_view(|cx| {
|
||||||
|
TestItem::new(cx)
|
||||||
|
.with_dirty(true)
|
||||||
|
.with_serialize(|| Some(Task::ready(Ok(()))))
|
||||||
|
});
|
||||||
|
let item2 = cx.new_view(|cx| {
|
||||||
|
TestItem::new(cx)
|
||||||
|
.with_dirty(true)
|
||||||
|
.with_project_items(&[TestProjectItem::new(1, "1.txt", cx)])
|
||||||
|
.with_serialize(|| Some(Task::ready(Ok(()))))
|
||||||
|
});
|
||||||
|
workspace.update(cx, |w, cx| {
|
||||||
|
w.add_item_to_active_pane(Box::new(item1.clone()), None, cx);
|
||||||
|
w.add_item_to_active_pane(Box::new(item2.clone()), None, cx);
|
||||||
|
});
|
||||||
|
let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx));
|
||||||
|
assert!(task.await.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_close_pane_items(cx: &mut TestAppContext) {
|
async fn test_close_pane_items(cx: &mut TestAppContext) {
|
||||||
init_test(cx);
|
init_test(cx);
|
||||||
@ -6352,7 +6573,6 @@ mod tests {
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
const TEST_PNG_KIND: &str = "TestPngItemView";
|
|
||||||
// View
|
// View
|
||||||
struct TestPngItemView {
|
struct TestPngItemView {
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
@ -6384,10 +6604,6 @@ mod tests {
|
|||||||
|
|
||||||
impl Item for TestPngItemView {
|
impl Item for TestPngItemView {
|
||||||
type Event = ();
|
type Event = ();
|
||||||
|
|
||||||
fn serialized_item_kind() -> Option<&'static str> {
|
|
||||||
Some(TEST_PNG_KIND)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
impl EventEmitter<()> for TestPngItemView {}
|
impl EventEmitter<()> for TestPngItemView {}
|
||||||
impl FocusableView for TestPngItemView {
|
impl FocusableView for TestPngItemView {
|
||||||
@ -6419,7 +6635,6 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const TEST_IPYNB_KIND: &str = "TestIpynbItemView";
|
|
||||||
// View
|
// View
|
||||||
struct TestIpynbItemView {
|
struct TestIpynbItemView {
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
@ -6451,10 +6666,6 @@ mod tests {
|
|||||||
|
|
||||||
impl Item for TestIpynbItemView {
|
impl Item for TestIpynbItemView {
|
||||||
type Event = ();
|
type Event = ();
|
||||||
|
|
||||||
fn serialized_item_kind() -> Option<&'static str> {
|
|
||||||
Some(TEST_IPYNB_KIND)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
impl EventEmitter<()> for TestIpynbItemView {}
|
impl EventEmitter<()> for TestIpynbItemView {}
|
||||||
impl FocusableView for TestIpynbItemView {
|
impl FocusableView for TestIpynbItemView {
|
||||||
@ -6490,14 +6701,10 @@ mod tests {
|
|||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
}
|
}
|
||||||
|
|
||||||
const TEST_ALTERNATE_PNG_KIND: &str = "TestAlternatePngItemView";
|
|
||||||
impl Item for TestAlternatePngItemView {
|
impl Item for TestAlternatePngItemView {
|
||||||
type Event = ();
|
type Event = ();
|
||||||
|
}
|
||||||
|
|
||||||
fn serialized_item_kind() -> Option<&'static str> {
|
|
||||||
Some(TEST_ALTERNATE_PNG_KIND)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl EventEmitter<()> for TestAlternatePngItemView {}
|
impl EventEmitter<()> for TestAlternatePngItemView {}
|
||||||
impl FocusableView for TestAlternatePngItemView {
|
impl FocusableView for TestAlternatePngItemView {
|
||||||
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
|
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
|
||||||
@ -6564,7 +6771,10 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Now we can check if the handle we got back errored or not
|
// Now we can check if the handle we got back errored or not
|
||||||
assert_eq!(handle.serialized_item_kind().unwrap(), TEST_PNG_KIND);
|
assert_eq!(
|
||||||
|
handle.to_any().entity_type(),
|
||||||
|
TypeId::of::<TestPngItemView>()
|
||||||
|
);
|
||||||
|
|
||||||
let handle = workspace
|
let handle = workspace
|
||||||
.update(cx, |workspace, cx| {
|
.update(cx, |workspace, cx| {
|
||||||
@ -6574,7 +6784,10 @@ mod tests {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(handle.serialized_item_kind().unwrap(), TEST_IPYNB_KIND);
|
assert_eq!(
|
||||||
|
handle.to_any().entity_type(),
|
||||||
|
TypeId::of::<TestIpynbItemView>()
|
||||||
|
);
|
||||||
|
|
||||||
let handle = workspace
|
let handle = workspace
|
||||||
.update(cx, |workspace, cx| {
|
.update(cx, |workspace, cx| {
|
||||||
@ -6622,8 +6835,8 @@ mod tests {
|
|||||||
|
|
||||||
// This _must_ be the second item registered
|
// This _must_ be the second item registered
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
handle.serialized_item_kind().unwrap(),
|
handle.to_any().entity_type(),
|
||||||
TEST_ALTERNATE_PNG_KIND
|
TypeId::of::<TestAlternatePngItemView>()
|
||||||
);
|
);
|
||||||
|
|
||||||
let handle = workspace
|
let handle = workspace
|
||||||
|
@ -964,13 +964,16 @@ mod tests {
|
|||||||
use editor::{display_map::DisplayRow, scroll::Autoscroll, DisplayPoint, Editor};
|
use editor::{display_map::DisplayRow, scroll::Autoscroll, DisplayPoint, Editor};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, Action, AnyWindowHandle, AppContext, AssetSource, BorrowAppContext, Entity,
|
actions, Action, AnyWindowHandle, AppContext, AssetSource, BorrowAppContext, Entity,
|
||||||
SemanticVersion, TestAppContext, VisualTestContext, WindowHandle,
|
SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext, WindowHandle,
|
||||||
};
|
};
|
||||||
use language::{LanguageMatcher, LanguageRegistry};
|
use language::{LanguageMatcher, LanguageRegistry};
|
||||||
use project::{Project, ProjectPath, WorktreeSettings};
|
use project::{project_settings::ProjectSettings, Project, ProjectPath, WorktreeSettings};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use settings::{handle_settings_file_changes, watch_config_file, SettingsStore};
|
use settings::{handle_settings_file_changes, watch_config_file, SettingsStore};
|
||||||
use std::path::{Path, PathBuf};
|
use std::{
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
use task::{RevealStrategy, SpawnInTerminal};
|
use task::{RevealStrategy, SpawnInTerminal};
|
||||||
use theme::{ThemeRegistry, ThemeSettings};
|
use theme::{ThemeRegistry, ThemeSettings};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
@ -1253,9 +1256,18 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_window_edit_state(cx: &mut TestAppContext) {
|
async fn test_window_edit_state_restoring_disabled(cx: &mut TestAppContext) {
|
||||||
let executor = cx.executor();
|
let executor = cx.executor();
|
||||||
let app_state = init_test(cx);
|
let app_state = init_test(cx);
|
||||||
|
|
||||||
|
cx.update(|cx| {
|
||||||
|
SettingsStore::update_global(cx, |store, cx| {
|
||||||
|
store.update_user_settings::<ProjectSettings>(cx, |settings| {
|
||||||
|
settings.session.restore_unsaved_buffers = false
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
app_state
|
app_state
|
||||||
.fs
|
.fs
|
||||||
.as_fake()
|
.as_fake()
|
||||||
@ -1335,6 +1347,9 @@ mod tests {
|
|||||||
close.await.unwrap();
|
close.await.unwrap();
|
||||||
assert!(!window_is_edited(window, cx));
|
assert!(!window_is_edited(window, cx));
|
||||||
|
|
||||||
|
// Advance the clock to ensure that the item has been serialized and dropped from the queue
|
||||||
|
cx.executor().advance_clock(Duration::from_secs(1));
|
||||||
|
|
||||||
// Opening the buffer again doesn't impact the window's edited state.
|
// Opening the buffer again doesn't impact the window's edited state.
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
open_paths(
|
open_paths(
|
||||||
@ -1346,6 +1361,22 @@ mod tests {
|
|||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
executor.run_until_parked();
|
||||||
|
|
||||||
|
window
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
let editor = workspace
|
||||||
|
.active_item(cx)
|
||||||
|
.unwrap()
|
||||||
|
.downcast::<Editor>()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
editor.update(cx, |editor, cx| {
|
||||||
|
assert_eq!(editor.text(cx), "hey");
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let editor = window
|
let editor = window
|
||||||
.read_with(cx, |workspace, cx| {
|
.read_with(cx, |workspace, cx| {
|
||||||
workspace
|
workspace
|
||||||
@ -1363,6 +1394,7 @@ mod tests {
|
|||||||
editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
|
editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
executor.run_until_parked();
|
||||||
assert!(window_is_edited(window, cx));
|
assert!(window_is_edited(window, cx));
|
||||||
|
|
||||||
// Ensure closing the window via the mouse gets preempted due to the
|
// Ensure closing the window via the mouse gets preempted due to the
|
||||||
@ -1377,6 +1409,102 @@ mod tests {
|
|||||||
assert_eq!(cx.update(|cx| cx.windows().len()), 0);
|
assert_eq!(cx.update(|cx| cx.windows().len()), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_window_edit_state_restoring_enabled(cx: &mut TestAppContext) {
|
||||||
|
let app_state = init_test(cx);
|
||||||
|
app_state
|
||||||
|
.fs
|
||||||
|
.as_fake()
|
||||||
|
.insert_tree("/root", json!({"a": "hey"}))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
cx.update(|cx| {
|
||||||
|
open_paths(
|
||||||
|
&[PathBuf::from("/root/a")],
|
||||||
|
app_state.clone(),
|
||||||
|
workspace::OpenOptions::default(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(cx.update(|cx| cx.windows().len()), 1);
|
||||||
|
|
||||||
|
// When opening the workspace, the window is not in a edited state.
|
||||||
|
let window = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
|
||||||
|
|
||||||
|
let window_is_edited = |window: WindowHandle<Workspace>, cx: &mut TestAppContext| {
|
||||||
|
cx.update(|cx| window.read(cx).unwrap().is_edited())
|
||||||
|
};
|
||||||
|
|
||||||
|
let editor = window
|
||||||
|
.read_with(cx, |workspace, cx| {
|
||||||
|
workspace
|
||||||
|
.active_item(cx)
|
||||||
|
.unwrap()
|
||||||
|
.downcast::<Editor>()
|
||||||
|
.unwrap()
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(!window_is_edited(window, cx));
|
||||||
|
|
||||||
|
// Editing a buffer marks the window as edited.
|
||||||
|
window
|
||||||
|
.update(cx, |_, cx| {
|
||||||
|
editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert!(window_is_edited(window, cx));
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
// Advance the clock to make sure the workspace is serialized
|
||||||
|
cx.executor().advance_clock(Duration::from_secs(1));
|
||||||
|
|
||||||
|
// When closing the window, no prompt shows up and the window is closed.
|
||||||
|
// buffer having unsaved changes.
|
||||||
|
assert!(!VisualTestContext::from_window(window.into(), cx).simulate_close());
|
||||||
|
cx.run_until_parked();
|
||||||
|
assert_eq!(cx.update(|cx| cx.windows().len()), 0);
|
||||||
|
|
||||||
|
// When we now reopen the window, the edited state and the edited buffer are back
|
||||||
|
cx.update(|cx| {
|
||||||
|
open_paths(
|
||||||
|
&[PathBuf::from("/root/a")],
|
||||||
|
app_state.clone(),
|
||||||
|
workspace::OpenOptions::default(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(cx.update(|cx| cx.windows().len()), 1);
|
||||||
|
assert!(cx.update(|cx| cx.active_window().is_some()));
|
||||||
|
|
||||||
|
// When opening the workspace, the window is not in a edited state.
|
||||||
|
let window = cx.update(|cx| cx.active_window().unwrap().downcast::<Workspace>().unwrap());
|
||||||
|
assert!(window_is_edited(window, cx));
|
||||||
|
|
||||||
|
window
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
let editor = workspace
|
||||||
|
.active_item(cx)
|
||||||
|
.unwrap()
|
||||||
|
.downcast::<editor::Editor>()
|
||||||
|
.unwrap();
|
||||||
|
editor.update(cx, |editor, cx| {
|
||||||
|
assert_eq!(editor.text(cx), "EDIThey");
|
||||||
|
assert!(editor.is_dirty(cx));
|
||||||
|
});
|
||||||
|
|
||||||
|
editor
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_new_empty_workspace(cx: &mut TestAppContext) {
|
async fn test_new_empty_workspace(cx: &mut TestAppContext) {
|
||||||
let app_state = init_test(cx);
|
let app_state = init_test(cx);
|
||||||
@ -2256,6 +2384,8 @@ mod tests {
|
|||||||
assert!(workspace.active_item(cx).is_none());
|
assert!(workspace.active_item(cx).is_none());
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
cx.run_until_parked();
|
||||||
editor_1.assert_released();
|
editor_1.assert_released();
|
||||||
editor_2.assert_released();
|
editor_2.assert_released();
|
||||||
buffer.assert_released();
|
buffer.assert_released();
|
||||||
|
Loading…
Reference in New Issue
Block a user