zed: Add ability to restore last session w/ multiple windows (#14965)
This adds the ability for Zed to restore multiple windows after restarting. It's now the default behavior. Release Notes: - Added ability to restore all windows that were open when Zed was quit. Previously only the last used workspace was restored. This is now the default behavior. To get back the old behavior, add the following to your settings: `{"restore_on_startup": "last_workspace"}` (Part of [#4985](https://github.com/zed-industries/zed/issues/4985) and [#4683](https://github.com/zed-industries/zed/issues/4683)) Demo: https://github.com/user-attachments/assets/57a375ec-0c6a-4724-97c4-3fea8f18bc2d --------- Co-authored-by: Nathan <nathan@zed.dev>
This commit is contained in:
parent
53f828df7d
commit
17ef9a367f
18 changed files with 660 additions and 172 deletions
|
@ -170,6 +170,7 @@ define_connection! {
|
|||
// display: Option<Uuid>, // Display id
|
||||
// fullscreen: Option<bool>, // Is the window fullscreen?
|
||||
// centered_layout: Option<bool>, // Is the Centered Layout mode activated?
|
||||
// session_id: Option<String>, // Session id
|
||||
// )
|
||||
//
|
||||
// pane_groups(
|
||||
|
@ -344,6 +345,9 @@ define_connection! {
|
|||
sql!(
|
||||
ALTER TABLE workspaces ADD COLUMN local_paths_order BLOB;
|
||||
),
|
||||
sql!(
|
||||
ALTER TABLE workspaces ADD COLUMN session_id TEXT DEFAULT NULL;
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -443,6 +447,7 @@ impl WorkspaceDb {
|
|||
centered_layout: centered_layout.unwrap_or(false),
|
||||
display,
|
||||
docks,
|
||||
session_id: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -536,6 +541,7 @@ impl WorkspaceDb {
|
|||
centered_layout: centered_layout.unwrap_or(false),
|
||||
display,
|
||||
docks,
|
||||
session_id: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -572,9 +578,10 @@ impl WorkspaceDb {
|
|||
bottom_dock_visible,
|
||||
bottom_dock_active_panel,
|
||||
bottom_dock_zoom,
|
||||
session_id,
|
||||
timestamp
|
||||
)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, CURRENT_TIMESTAMP)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT DO
|
||||
UPDATE SET
|
||||
local_paths = ?2,
|
||||
|
@ -588,8 +595,9 @@ impl WorkspaceDb {
|
|||
bottom_dock_visible = ?10,
|
||||
bottom_dock_active_panel = ?11,
|
||||
bottom_dock_zoom = ?12,
|
||||
session_id = ?13,
|
||||
timestamp = CURRENT_TIMESTAMP
|
||||
))?((workspace.id, &local_paths, &local_paths_order, workspace.docks))
|
||||
))?((workspace.id, &local_paths, &local_paths_order, workspace.docks, workspace.session_id))
|
||||
.context("Updating workspace")?;
|
||||
}
|
||||
SerializedWorkspaceLocation::DevServer(dev_server_project) => {
|
||||
|
@ -675,6 +683,15 @@ impl WorkspaceDb {
|
|||
}
|
||||
}
|
||||
|
||||
query! {
|
||||
fn session_workspace_locations(session_id: String) -> Result<Vec<LocalPaths>> {
|
||||
SELECT local_paths
|
||||
FROM workspaces
|
||||
WHERE session_id = ?1 AND dev_server_project_id IS NULL
|
||||
ORDER BY timestamp DESC
|
||||
}
|
||||
}
|
||||
|
||||
query! {
|
||||
fn dev_server_projects() -> Result<Vec<SerializedDevServerProject>> {
|
||||
SELECT id, path, dev_server_name
|
||||
|
@ -770,6 +787,23 @@ impl WorkspaceDb {
|
|||
.next())
|
||||
}
|
||||
|
||||
pub fn last_session_workspace_locations(
|
||||
&self,
|
||||
last_session_id: &str,
|
||||
) -> Result<Vec<LocalPaths>> {
|
||||
let mut result = Vec::new();
|
||||
|
||||
for location in self.session_workspace_locations(last_session_id.to_owned())? {
|
||||
if location.paths().iter().all(|path| path.exists())
|
||||
&& location.paths().iter().any(|path| path.is_dir())
|
||||
{
|
||||
result.push(location);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
|
||||
Ok(self
|
||||
.get_pane_group(workspace_id, None)?
|
||||
|
@ -983,6 +1017,7 @@ impl WorkspaceDb {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::*;
|
||||
use db::open_test_db;
|
||||
use gpui;
|
||||
|
@ -1065,6 +1100,7 @@ mod tests {
|
|||
display: Default::default(),
|
||||
docks: Default::default(),
|
||||
centered_layout: false,
|
||||
session_id: None,
|
||||
};
|
||||
|
||||
let workspace_2 = SerializedWorkspace {
|
||||
|
@ -1075,6 +1111,7 @@ mod tests {
|
|||
display: Default::default(),
|
||||
docks: Default::default(),
|
||||
centered_layout: false,
|
||||
session_id: None,
|
||||
};
|
||||
|
||||
db.save_workspace(workspace_1.clone()).await;
|
||||
|
@ -1177,6 +1214,7 @@ mod tests {
|
|||
display: Default::default(),
|
||||
docks: Default::default(),
|
||||
centered_layout: false,
|
||||
session_id: None,
|
||||
};
|
||||
|
||||
db.save_workspace(workspace.clone()).await;
|
||||
|
@ -1209,6 +1247,7 @@ mod tests {
|
|||
display: Default::default(),
|
||||
docks: Default::default(),
|
||||
centered_layout: false,
|
||||
session_id: None,
|
||||
};
|
||||
|
||||
let mut workspace_2 = SerializedWorkspace {
|
||||
|
@ -1219,6 +1258,7 @@ mod tests {
|
|||
display: Default::default(),
|
||||
docks: Default::default(),
|
||||
centered_layout: false,
|
||||
session_id: None,
|
||||
};
|
||||
|
||||
db.save_workspace(workspace_1.clone()).await;
|
||||
|
@ -1259,6 +1299,7 @@ mod tests {
|
|||
display: Default::default(),
|
||||
docks: Default::default(),
|
||||
centered_layout: false,
|
||||
session_id: None,
|
||||
};
|
||||
|
||||
db.save_workspace(workspace_3.clone()).await;
|
||||
|
@ -1279,6 +1320,75 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_session_workspace_locations() {
|
||||
env_logger::try_init().ok();
|
||||
|
||||
let db = WorkspaceDb(open_test_db("test_serializing_workspaces_session_id").await);
|
||||
|
||||
let workspace_1 = SerializedWorkspace {
|
||||
id: WorkspaceId(1),
|
||||
location: SerializedWorkspaceLocation::from_local_paths(["/tmp1"]),
|
||||
center_group: Default::default(),
|
||||
window_bounds: Default::default(),
|
||||
display: Default::default(),
|
||||
docks: Default::default(),
|
||||
centered_layout: false,
|
||||
session_id: Some("session-id-1".to_owned()),
|
||||
};
|
||||
|
||||
let workspace_2 = SerializedWorkspace {
|
||||
id: WorkspaceId(2),
|
||||
location: SerializedWorkspaceLocation::from_local_paths(["/tmp2"]),
|
||||
center_group: Default::default(),
|
||||
window_bounds: Default::default(),
|
||||
display: Default::default(),
|
||||
docks: Default::default(),
|
||||
centered_layout: false,
|
||||
session_id: Some("session-id-1".to_owned()),
|
||||
};
|
||||
|
||||
let workspace_3 = SerializedWorkspace {
|
||||
id: WorkspaceId(3),
|
||||
location: SerializedWorkspaceLocation::from_local_paths(["/tmp3"]),
|
||||
center_group: Default::default(),
|
||||
window_bounds: Default::default(),
|
||||
display: Default::default(),
|
||||
docks: Default::default(),
|
||||
centered_layout: false,
|
||||
session_id: Some("session-id-2".to_owned()),
|
||||
};
|
||||
|
||||
let workspace_4 = SerializedWorkspace {
|
||||
id: WorkspaceId(4),
|
||||
location: SerializedWorkspaceLocation::from_local_paths(["/tmp4"]),
|
||||
center_group: Default::default(),
|
||||
window_bounds: Default::default(),
|
||||
display: Default::default(),
|
||||
docks: Default::default(),
|
||||
centered_layout: false,
|
||||
session_id: None,
|
||||
};
|
||||
|
||||
db.save_workspace(workspace_1.clone()).await;
|
||||
db.save_workspace(workspace_2.clone()).await;
|
||||
db.save_workspace(workspace_3.clone()).await;
|
||||
db.save_workspace(workspace_4.clone()).await;
|
||||
|
||||
let locations = db
|
||||
.session_workspace_locations("session-id-1".to_owned())
|
||||
.unwrap();
|
||||
assert_eq!(locations.len(), 2);
|
||||
assert_eq!(locations[0], LocalPaths::new(["/tmp1"]));
|
||||
assert_eq!(locations[1], LocalPaths::new(["/tmp2"]));
|
||||
|
||||
let locations = db
|
||||
.session_workspace_locations("session-id-2".to_owned())
|
||||
.unwrap();
|
||||
assert_eq!(locations.len(), 1);
|
||||
assert_eq!(locations[0], LocalPaths::new(["/tmp3"]));
|
||||
}
|
||||
|
||||
use crate::persistence::model::SerializedWorkspace;
|
||||
use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
|
||||
|
||||
|
@ -1294,6 +1404,7 @@ mod tests {
|
|||
display: Default::default(),
|
||||
docks: Default::default(),
|
||||
centered_layout: false,
|
||||
session_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -58,6 +58,7 @@ impl Column for LocalPaths {
|
|||
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
|
||||
let path_blob = statement.column_blob(start_index)?;
|
||||
let paths: Arc<Vec<PathBuf>> = if path_blob.is_empty() {
|
||||
println!("path blog is empty");
|
||||
Default::default()
|
||||
} else {
|
||||
bincode::deserialize(path_blob).context("Bincode deserialization of paths failed")?
|
||||
|
@ -214,6 +215,7 @@ pub(crate) struct SerializedWorkspace {
|
|||
pub(crate) centered_layout: bool,
|
||||
pub(crate) display: Option<Uuid>,
|
||||
pub(crate) docks: DockStructure,
|
||||
pub(crate) session_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Default)]
|
||||
|
|
|
@ -58,6 +58,7 @@ pub use persistence::{
|
|||
use postage::stream::Stream;
|
||||
use project::{DirectoryLister, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId};
|
||||
use serde::Deserialize;
|
||||
use session::Session;
|
||||
use settings::Settings;
|
||||
use shared_screen::SharedScreen;
|
||||
use sqlez::{
|
||||
|
@ -536,6 +537,7 @@ pub struct AppState {
|
|||
pub fs: Arc<dyn fs::Fs>,
|
||||
pub build_window_options: fn(Option<Uuid>, &mut AppContext) -> WindowOptions,
|
||||
pub node_runtime: Arc<dyn NodeRuntime>,
|
||||
pub session: Session,
|
||||
}
|
||||
|
||||
struct GlobalAppState(Weak<AppState>);
|
||||
|
@ -569,6 +571,7 @@ impl AppState {
|
|||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn test(cx: &mut AppContext) -> Arc<Self> {
|
||||
use node_runtime::FakeNodeRuntime;
|
||||
use session::Session;
|
||||
use settings::SettingsStore;
|
||||
use ui::Context as _;
|
||||
|
||||
|
@ -582,6 +585,7 @@ impl AppState {
|
|||
let clock = Arc::new(clock::FakeSystemClock::default());
|
||||
let http_client = http::FakeHttpClient::with_404_response();
|
||||
let client = Client::new(clock, http_client.clone(), cx);
|
||||
let session = Session::test();
|
||||
let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx));
|
||||
let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx));
|
||||
|
||||
|
@ -597,6 +601,7 @@ impl AppState {
|
|||
workspace_store,
|
||||
node_runtime: FakeNodeRuntime::new(),
|
||||
build_window_options: |_, _| Default::default(),
|
||||
session,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -664,6 +669,7 @@ pub enum Event {
|
|||
ZoomChanged,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum OpenVisible {
|
||||
All,
|
||||
None,
|
||||
|
@ -730,6 +736,7 @@ pub struct Workspace {
|
|||
Option<Box<dyn Fn(&mut Self, &mut ViewContext<Self>) -> AnyElement>>,
|
||||
serializable_items_tx: UnboundedSender<Box<dyn SerializableItemHandle>>,
|
||||
_items_serializer: Task<Result<()>>,
|
||||
session_id: Option<String>,
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for Workspace {}
|
||||
|
@ -908,6 +915,8 @@ impl Workspace {
|
|||
|
||||
let modal_layer = cx.new_view(|_| ModalLayer::new());
|
||||
|
||||
let session_id = app_state.session.id().to_owned();
|
||||
|
||||
let mut active_call = None;
|
||||
if let Some(call) = ActiveCall::try_global(cx) {
|
||||
let call = call.clone();
|
||||
|
@ -1023,6 +1032,7 @@ impl Workspace {
|
|||
render_disconnected_overlay: None,
|
||||
serializable_items_tx,
|
||||
_items_serializer,
|
||||
session_id: Some(session_id),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1654,10 +1664,20 @@ impl Workspace {
|
|||
}
|
||||
}
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.save_all_internal(SaveIntent::Close, cx)
|
||||
})?
|
||||
.await
|
||||
let save_result = this
|
||||
.update(&mut cx, |this, cx| {
|
||||
this.save_all_internal(SaveIntent::Close, cx)
|
||||
})?
|
||||
.await;
|
||||
|
||||
// If we're not quitting, but closing, we remove the workspace from
|
||||
// the current session.
|
||||
if !quitting && save_result.as_ref().map_or(false, |&res| res) {
|
||||
this.update(&mut cx, |this, cx| this.remove_from_session(cx))?
|
||||
.await;
|
||||
}
|
||||
|
||||
save_result
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -3838,6 +3858,11 @@ impl Workspace {
|
|||
}
|
||||
}
|
||||
|
||||
fn remove_from_session(&mut self, cx: &mut WindowContext) -> Task<()> {
|
||||
self.session_id.take();
|
||||
self.serialize_workspace_internal(cx)
|
||||
}
|
||||
|
||||
fn force_remove_pane(&mut self, pane: &View<Pane>, cx: &mut ViewContext<Workspace>) {
|
||||
self.panes.retain(|p| p != pane);
|
||||
self.panes
|
||||
|
@ -3992,7 +4017,6 @@ impl Workspace {
|
|||
None
|
||||
};
|
||||
|
||||
// don't save workspace state for the empty workspace.
|
||||
if let Some(location) = location {
|
||||
let center_group = build_serialized_pane_group(&self.center.root, cx);
|
||||
let docks = build_serialized_docks(self, cx);
|
||||
|
@ -4005,6 +4029,7 @@ impl Workspace {
|
|||
display: Default::default(),
|
||||
docks,
|
||||
centered_layout: self.centered_layout,
|
||||
session_id: self.session_id.clone(),
|
||||
};
|
||||
return cx.spawn(|_| persistence::DB.save_workspace(serialized_workspace));
|
||||
}
|
||||
|
@ -4258,6 +4283,7 @@ impl Workspace {
|
|||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub fn test_new(project: Model<Project>, cx: &mut ViewContext<Self>) -> Self {
|
||||
use node_runtime::FakeNodeRuntime;
|
||||
use session::Session;
|
||||
|
||||
let client = project.read(cx).client();
|
||||
let user_store = project.read(cx).user_store();
|
||||
|
@ -4272,6 +4298,7 @@ impl Workspace {
|
|||
fs: project.read(cx).fs().clone(),
|
||||
build_window_options: |_, _| Default::default(),
|
||||
node_runtime: FakeNodeRuntime::new(),
|
||||
session: Session::test(),
|
||||
});
|
||||
let workspace = Self::new(Default::default(), project, app_state, cx);
|
||||
workspace.active_pane.update(cx, |pane, cx| pane.focus(cx));
|
||||
|
@ -4873,6 +4900,11 @@ pub async fn last_opened_workspace_paths() -> Option<LocalPaths> {
|
|||
DB.last_workspace().await.log_err().flatten()
|
||||
}
|
||||
|
||||
pub fn last_session_workspace_locations(last_session_id: &str) -> Option<Vec<LocalPaths>> {
|
||||
DB.last_session_workspace_locations(last_session_id)
|
||||
.log_err()
|
||||
}
|
||||
|
||||
actions!(collab, [OpenChannelNotes]);
|
||||
actions!(zed, [OpenLog]);
|
||||
|
||||
|
|
|
@ -47,8 +47,10 @@ pub enum RestoreOnStartupBehaviour {
|
|||
/// Always start with an empty editor
|
||||
None,
|
||||
/// Restore the workspace that was closed last.
|
||||
#[default]
|
||||
LastWorkspace,
|
||||
/// Restore all workspaces that were open when quitting Zed.
|
||||
#[default]
|
||||
LastSession,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||
|
@ -74,8 +76,8 @@ pub struct WorkspaceSettingsContent {
|
|||
/// Default: off
|
||||
pub autosave: Option<AutosaveSetting>,
|
||||
/// Controls previous session restoration in freshly launched Zed instance.
|
||||
/// Values: none, last_workspace
|
||||
/// Default: last_workspace
|
||||
/// Values: none, last_workspace, last_session
|
||||
/// Default: last_session
|
||||
pub restore_on_startup: Option<RestoreOnStartupBehaviour>,
|
||||
/// The size of the workspace split drop targets on the outer edges.
|
||||
/// Given as a fraction that will be multiplied by the smaller dimension of the workspace.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue