WIP fixing dock problems

Mikayla Maki created

Change summary

crates/auto_update/src/update_notification.rs |   2 
crates/client/src/telemetry.rs                |   2 
crates/db/src/db.rs                           | 175 +++++++++++++-------
crates/sqlez/src/migrations.rs                |   3 
crates/sqlez/src/thread_safe_connection.rs    |   1 
crates/util/src/channel.rs                    |  10 +
crates/workspace/Cargo.toml                   |   1 
crates/workspace/src/dock.rs                  |   8 
crates/workspace/src/persistence.rs           |  33 ++-
crates/workspace/src/persistence/model.rs     |  29 ++-
crates/workspace/src/workspace.rs             | 103 +++++++----
crates/zed/src/zed.rs                         |   2 
12 files changed, 233 insertions(+), 136 deletions(-)

Detailed changes

crates/auto_update/src/update_notification.rs 🔗

@@ -30,7 +30,7 @@ impl View for UpdateNotification {
         let theme = cx.global::<Settings>().theme.clone();
         let theme = &theme.update_notification;
 
-        let app_name = cx.global::<ReleaseChannel>().name();
+        let app_name = cx.global::<ReleaseChannel>().display_name();
 
         MouseEventHandler::<ViewReleaseNotes>::new(0, cx, |state, cx| {
             Flex::column()

crates/client/src/telemetry.rs 🔗

@@ -106,7 +106,7 @@ impl Telemetry {
     pub fn new(client: Arc<dyn HttpClient>, cx: &AppContext) -> Arc<Self> {
         let platform = cx.platform();
         let release_channel = if cx.has_global::<ReleaseChannel>() {
-            Some(cx.global::<ReleaseChannel>().name())
+            Some(cx.global::<ReleaseChannel>().display_name())
         } else {
             None
         };

crates/db/src/db.rs 🔗

@@ -36,6 +36,8 @@ const DB_INITIALIZE_QUERY: &'static str = sql!(
 
 const FALLBACK_DB_NAME: &'static str = "FALLBACK_MEMORY_DB";
 
+const DB_FILE_NAME: &'static str = "db.sqlite";
+
 lazy_static::lazy_static! {
     static ref DB_FILE_OPERATIONS: Mutex<()> = Mutex::new(());
     static ref DB_WIPED: RwLock<bool> = RwLock::new(false);
@@ -48,7 +50,8 @@ lazy_static::lazy_static! {
 /// is moved to a backup folder and a new one is created. If that fails, a shared in memory db is created.
 /// In either case, static variables are set so that the user can be notified.
 pub async fn open_db<M: Migrator + 'static>(wipe_db: bool, db_dir: &Path, release_channel: &ReleaseChannel) -> ThreadSafeConnection<M> {
-    let main_db_dir = db_dir.join(Path::new(&format!("0-{}", release_channel.name())));
+    let release_channel_name = release_channel.dev_name();
+    let main_db_dir = db_dir.join(Path::new(&format!("0-{}", release_channel_name)));
 
     // If WIPE_DB, delete 0-{channel}
     if release_channel == &ReleaseChannel::Dev
@@ -77,7 +80,7 @@ pub async fn open_db<M: Migrator + 'static>(wipe_db: bool, db_dir: &Path, releas
         
         // If no db folder, create one at 0-{channel}
         create_dir_all(&main_db_dir).context("Could not create db directory")?;
-        let db_path = main_db_dir.join(Path::new("db.sqlite"));
+        let db_path = main_db_dir.join(Path::new(DB_FILE_NAME));
         
         // Optimistically open databases in parallel
         if !DB_FILE_OPERATIONS.is_locked() {
@@ -104,7 +107,7 @@ pub async fn open_db<M: Migrator + 'static>(wipe_db: bool, db_dir: &Path, releas
         let backup_db_dir = db_dir.join(Path::new(&format!(
             "{}-{}",
             backup_timestamp,
-            release_channel.name(),
+            release_channel_name,
         )));
 
         std::fs::rename(&main_db_dir, &backup_db_dir)
@@ -118,7 +121,7 @@ pub async fn open_db<M: Migrator + 'static>(wipe_db: bool, db_dir: &Path, releas
         
         // Create a new 0-{channel}
         create_dir_all(&main_db_dir).context("Should be able to create the database directory")?;
-        let db_path = main_db_dir.join(Path::new("db.sqlite"));
+        let db_path = main_db_dir.join(Path::new(DB_FILE_NAME));
 
         // Try again
         open_main_db(&db_path).await.context("Could not newly created db")
@@ -240,86 +243,130 @@ macro_rules! define_connection {
 
 #[cfg(test)]
 mod tests {
-    use std::thread;
+    use std::{thread, fs};
 
-    use sqlez::domain::Domain;
+    use sqlez::{domain::Domain, connection::Connection};
     use sqlez_macros::sql;
     use tempdir::TempDir;
     use util::channel::ReleaseChannel;
 
-    use crate::open_db;
-    
-    enum TestDB {}
-    
-    impl Domain for TestDB {
-        fn name() -> &'static str {
-            "db_tests"
-        }
-
-        fn migrations() -> &'static [&'static str] {
-            &[sql!(
-                CREATE TABLE test(value);
-            )]
-        }
-    }
+    use crate::{open_db, DB_FILE_NAME};
     
     // Test that wipe_db exists and works and gives a new db
-    #[test]
-    fn test_wipe_db() {
-        env_logger::try_init().ok();
+    #[gpui::test]
+    async fn test_wipe_db() {
+        enum TestDB {}
         
-        smol::block_on(async {
-            let tempdir = TempDir::new("DbTests").unwrap();
-            
-            let test_db = open_db::<TestDB>(false, tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
-            test_db.write(|connection|  
-                connection.exec(sql!(
-                    INSERT INTO test(value) VALUES (10)
-                )).unwrap()().unwrap()
-            ).await;
-            drop(test_db);
-            
-            let mut guards = vec![];
-            for _ in 0..5 {
-                let path = tempdir.path().to_path_buf();
-                let guard = thread::spawn(move || smol::block_on(async {
-                    let test_db = open_db::<TestDB>(true, &path, &ReleaseChannel::Dev).await;
-                    
-                    assert!(test_db.select_row::<()>(sql!(SELECT value FROM test)).unwrap()().unwrap().is_none())
-                }));
-                
-                guards.push(guard);
+        impl Domain for TestDB {
+            fn name() -> &'static str {
+                "db_tests"
             }
             
-            for guard in guards {
-                guard.join().unwrap();
+            fn migrations() -> &'static [&'static str] {
+                &[sql!(
+                    CREATE TABLE test(value);
+                )]
             }
-        })
-    }
-
-    // Test a file system failure (like in create_dir_all())
-    #[test]
-    fn test_file_system_failure() {
+        }
         
-    }
-    
-    // Test happy path where everything exists and opens
-    #[test]
-    fn test_open_db() {
+        let tempdir = TempDir::new("DbTests").unwrap();
         
+        // Create a db and insert a marker value
+        let test_db = open_db::<TestDB>(false, tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
+        test_db.write(|connection|  
+            connection.exec(sql!(
+                INSERT INTO test(value) VALUES (10)
+            )).unwrap()().unwrap()
+        ).await;
+        drop(test_db);
+        
+        // Opening db with wipe clears once and removes the marker value
+        let mut guards = vec![];
+        for _ in 0..5 {
+            let path = tempdir.path().to_path_buf();
+            let guard = thread::spawn(move || smol::block_on(async {
+                let test_db = open_db::<TestDB>(true, &path, &ReleaseChannel::Dev).await;
+                
+                assert!(test_db.select_row::<()>(sql!(SELECT value FROM test)).unwrap()().unwrap().is_none())
+            }));
+            
+            guards.push(guard);
+        }
+        
+        for guard in guards {
+            guard.join().unwrap();
+        }
     }
-    
+        
     // Test bad migration panics
-    #[test]
-    fn test_bad_migration_panics() {
+    #[gpui::test]
+    #[should_panic]
+    async fn test_bad_migration_panics() {
+        enum BadDB {}
         
+        impl Domain for BadDB {
+            fn name() -> &'static str {
+                "db_tests"
+            }
+            
+            fn migrations() -> &'static [&'static str] {
+                &[sql!(CREATE TABLE test(value);),
+                    // failure because test already exists
+                  sql!(CREATE TABLE test(value);)]
+            }
+        }
+       
+        let tempdir = TempDir::new("DbTests").unwrap();
+        let _bad_db = open_db::<BadDB>(false, tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
     }
     
     /// Test that DB exists but corrupted (causing recreate)
-    #[test]
-    fn test_db_corruption() {
+    #[gpui::test]
+    async fn test_db_corruption() {
+        enum CorruptedDB {}
+        
+        impl Domain for CorruptedDB {
+            fn name() -> &'static str {
+                "db_tests"
+            }
+            
+            fn migrations() -> &'static [&'static str] {
+                &[sql!(CREATE TABLE test(value);)]
+            }
+        }
+        
+        enum GoodDB {}
+        
+        impl Domain for GoodDB {
+            fn name() -> &'static str {
+                "db_tests" //Notice same name
+            }
+            
+            fn migrations() -> &'static [&'static str] {
+                &[sql!(CREATE TABLE test2(value);)] //But different migration
+            }
+        }
+       
+        let tempdir = TempDir::new("DbTests").unwrap();
+        {
+            let corrupt_db = open_db::<CorruptedDB>(false, tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
+            assert!(corrupt_db.persistent());
+        }
+        
+        let good_db = open_db::<GoodDB>(false, tempdir.path(), &util::channel::ReleaseChannel::Dev).await;
+        assert!(good_db.select_row::<usize>("SELECT * FROM test2").unwrap()().unwrap().is_none());
+        
+        let mut corrupted_backup_dir = fs::read_dir(
+            tempdir.path()
+        ).unwrap().find(|entry| {
+            !entry.as_ref().unwrap().file_name().to_str().unwrap().starts_with("0")
+        }
+        ).unwrap().unwrap().path();
+        corrupted_backup_dir.push(DB_FILE_NAME);
         
+        dbg!(&corrupted_backup_dir);
         
-        // open_db(db_dir, release_channel)
+        let backup = Connection::open_file(&corrupted_backup_dir.to_string_lossy());
+        assert!(backup.select_row::<usize>("SELECT * FROM test").unwrap()().unwrap().is_none());
     }
 }

crates/sqlez/src/migrations.rs 🔗

@@ -12,7 +12,6 @@ use crate::connection::Connection;
 impl Connection {
     pub fn migrate(&self, domain: &'static str, migrations: &[&'static str]) -> Result<()> {
         self.with_savepoint("migrating", || {
-            println!("Processing domain");
             // Setup the migrations table unconditionally
             self.exec(indoc! {"
                 CREATE TABLE IF NOT EXISTS migrations (
@@ -44,13 +43,11 @@ impl Connection {
                             {}", domain, index, completed_migration, migration}));
                     } else {
                         // Migration already run. Continue
-                        println!("Migration already run");
                         continue;
                     }
                 }
 
                 self.exec(migration)?()?;
-                println!("Ran migration");
                 store_completed_migration((domain, index, *migration))?;
             }
 

crates/sqlez/src/thread_safe_connection.rs 🔗

@@ -96,7 +96,6 @@ impl<M: Migrator> ThreadSafeConnectionBuilder<M> {
                         .with_savepoint("thread_safe_multi_migration", || M::migrate(connection));
 
                     if migration_result.is_ok() {
-                        println!("Migration succeded");
                         break;
                     }
                 }

crates/util/src/channel.rs 🔗

@@ -22,11 +22,19 @@ pub enum ReleaseChannel {
 }
 
 impl ReleaseChannel {
-    pub fn name(&self) -> &'static str {
+    pub fn display_name(&self) -> &'static str {
         match self {
             ReleaseChannel::Dev => "Zed Dev",
             ReleaseChannel::Preview => "Zed Preview",
             ReleaseChannel::Stable => "Zed",
         }
     }
+
+    pub fn dev_name(&self) -> &'static str {
+        match self {
+            ReleaseChannel::Dev => "dev",
+            ReleaseChannel::Preview => "preview",
+            ReleaseChannel::Stable => "stable",
+        }
+    }
 }

crates/workspace/Cargo.toml 🔗

@@ -46,7 +46,6 @@ serde_json = { version = "1.0", features = ["preserve_order"] }
 smallvec = { version = "1.6", features = ["union"] }
 indoc = "1.0.4"
 
-
 [dev-dependencies]
 call = { path = "../call", features = ["test-support"] }
 client = { path = "../client", features = ["test-support"] }

crates/workspace/src/dock.rs 🔗

@@ -175,16 +175,21 @@ impl Dock {
         new_position: DockPosition,
         cx: &mut ViewContext<Workspace>,
     ) {
+        dbg!("starting", &new_position);
         workspace.dock.position = new_position;
         // Tell the pane about the new anchor position
         workspace.dock.pane.update(cx, |pane, cx| {
+            dbg!("setting docked");
             pane.set_docked(Some(new_position.anchor()), cx)
         });
 
         if workspace.dock.position.is_visible() {
+            dbg!("dock is visible");
             // Close the right sidebar if the dock is on the right side and the right sidebar is open
             if workspace.dock.position.anchor() == DockAnchor::Right {
+                dbg!("dock anchor is right");
                 if workspace.right_sidebar().read(cx).is_open() {
+                    dbg!("Toggling right sidebar");
                     workspace.toggle_sidebar(SidebarSide::Right, cx);
                 }
             }
@@ -194,8 +199,10 @@ impl Dock {
             if pane.read(cx).items().next().is_none() {
                 let item_to_add = (workspace.dock.default_item_factory)(workspace, cx);
                 // Adding the item focuses the pane by default
+                dbg!("Adding item to dock");
                 Pane::add_item(workspace, &pane, item_to_add, true, true, None, cx);
             } else {
+                dbg!("just focusing dock");
                 cx.focus(pane);
             }
         } else if let Some(last_active_center_pane) = workspace
@@ -207,6 +214,7 @@ impl Dock {
         }
         cx.emit(crate::Event::DockAnchorChanged);
         workspace.serialize_workspace(cx);
+        dbg!("Serializing workspace after dock position changed");
         cx.notify();
     }
 

crates/workspace/src/persistence.rs 🔗

@@ -27,7 +27,7 @@ define_connection! {
                 dock_visible INTEGER, // Boolean
                 dock_anchor TEXT, // Enum: 'Bottom' / 'Right' / 'Expanded'
                 dock_pane INTEGER, // NULL indicates that we don't have a dock pane yet
-                project_panel_open INTEGER, //Boolean
+                left_sidebar_open INTEGER, //Boolean
                 timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
                 FOREIGN KEY(dock_pane) REFERENCES panes(pane_id)
             ) STRICT;
@@ -91,7 +91,7 @@ impl WorkspaceDb {
 
         // Note that we re-assign the workspace_id here in case it's empty
         // and we've grabbed the most recent workspace
-        let (workspace_id, workspace_location, project_panel_open, dock_position): (
+        let (workspace_id, workspace_location, left_sidebar_open, dock_position): (
             WorkspaceId,
             WorkspaceLocation,
             bool,
@@ -99,12 +99,12 @@ impl WorkspaceDb {
         ) = iife!({
             if worktree_roots.len() == 0 {
                 self.select_row(sql!(
-                    SELECT workspace_id, workspace_location, project_panel_open, dock_visible, dock_anchor
+                    SELECT workspace_id, workspace_location, left_sidebar_open, dock_visible, dock_anchor
                     FROM workspaces
                     ORDER BY timestamp DESC LIMIT 1))?()?
             } else {
                 self.select_row_bound(sql!(
-                    SELECT workspace_id, workspace_location, project_panel_open, dock_visible, dock_anchor
+                    SELECT workspace_id, workspace_location, left_sidebar_open, dock_visible, dock_anchor
                     FROM workspaces 
                     WHERE workspace_location = ?))?(&workspace_location)?
             }
@@ -125,7 +125,7 @@ impl WorkspaceDb {
                 .context("Getting center group")
                 .log_err()?,
             dock_position,
-            project_panel_open
+            left_sidebar_open
         })
     }
 
@@ -151,7 +151,7 @@ impl WorkspaceDb {
                         INSERT INTO workspaces(
                             workspace_id,
                             workspace_location,
-                            project_panel_open,
+                            left_sidebar_open,
                             dock_visible,
                             dock_anchor,
                             timestamp
@@ -160,11 +160,11 @@ impl WorkspaceDb {
                         ON CONFLICT DO
                             UPDATE SET
                             workspace_location = ?2,
-                            project_panel_open = ?3,
+                            left_sidebar_open = ?3,
                             dock_visible = ?4,
                             dock_anchor = ?5,
                             timestamp = CURRENT_TIMESTAMP
-                ))?((workspace.id, &workspace.location, workspace.project_panel_open, workspace.dock_position))
+                ))?((workspace.id, &workspace.location, workspace.left_sidebar_open, workspace.dock_position))
                 .context("Updating workspace")?;
 
                 // Save center pane group and dock pane
@@ -198,7 +198,8 @@ impl WorkspaceDb {
     query! {
         pub fn recent_workspaces(limit: usize) -> Result<Vec<(WorkspaceId, WorkspaceLocation)>> {
             SELECT workspace_id, workspace_location 
-            FROM workspaces 
+            FROM workspaces
+            WHERE workspace_location IS NOT NULL
             ORDER BY timestamp DESC 
             LIMIT ?
         }
@@ -458,7 +459,7 @@ mod tests {
             dock_position: crate::dock::DockPosition::Shown(DockAnchor::Bottom),
             center_group: Default::default(),
             dock_pane: Default::default(),
-            project_panel_open: true
+            left_sidebar_open: true
         };
 
         let mut workspace_2 = SerializedWorkspace {
@@ -467,7 +468,7 @@ mod tests {
             dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Expanded),
             center_group: Default::default(),
             dock_pane: Default::default(),
-            project_panel_open: false
+            left_sidebar_open: false
         };
 
         db.save_workspace(workspace_1.clone()).await;
@@ -573,7 +574,7 @@ mod tests {
             dock_position: DockPosition::Shown(DockAnchor::Bottom),
             center_group,
             dock_pane,
-            project_panel_open: true
+            left_sidebar_open: true
         };
 
         db.save_workspace(workspace.clone()).await;
@@ -601,7 +602,7 @@ mod tests {
             dock_position: crate::dock::DockPosition::Shown(DockAnchor::Bottom),
             center_group: Default::default(),
             dock_pane: Default::default(),
-            project_panel_open: true,
+            left_sidebar_open: true,
         };
 
         let mut workspace_2 = SerializedWorkspace {
@@ -610,7 +611,7 @@ mod tests {
             dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Expanded),
             center_group: Default::default(),
             dock_pane: Default::default(),
-            project_panel_open: false,
+            left_sidebar_open: false,
         };
 
         db.save_workspace(workspace_1.clone()).await;
@@ -646,7 +647,7 @@ mod tests {
             dock_position: DockPosition::Shown(DockAnchor::Right),
             center_group: Default::default(),
             dock_pane: Default::default(),
-            project_panel_open: false
+            left_sidebar_open: false
         };
 
         db.save_workspace(workspace_3.clone()).await;
@@ -681,7 +682,7 @@ mod tests {
             dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Right),
             center_group: center_group.clone(),
             dock_pane,
-            project_panel_open: true
+            left_sidebar_open: true
         }
     }
 

crates/workspace/src/persistence/model.rs 🔗

@@ -65,7 +65,7 @@ pub struct SerializedWorkspace {
     pub dock_position: DockPosition,
     pub center_group: SerializedPaneGroup,
     pub dock_pane: SerializedPane,
-    pub project_panel_open: bool,
+    pub left_sidebar_open: bool,
 }
 
 #[derive(Debug, PartialEq, Eq, Clone)]
@@ -95,26 +95,33 @@ impl SerializedPaneGroup {
         workspace_id: WorkspaceId,
         workspace: &ViewHandle<Workspace>,
         cx: &mut AsyncAppContext,
-    ) -> (Member, Option<ViewHandle<Pane>>) {
+    ) -> Option<(Member, Option<ViewHandle<Pane>>)> {
         match self {
             SerializedPaneGroup::Group { axis, children } => {
                 let mut current_active_pane = None;
                 let mut members = Vec::new();
                 for child in children {
-                    let (new_member, active_pane) = child
+                    if let Some((new_member, active_pane)) = child
                         .deserialize(project, workspace_id, workspace, cx)
-                        .await;
-                    members.push(new_member);
+                        .await
+                    {
+                        members.push(new_member);
 
-                    current_active_pane = current_active_pane.or(active_pane);
+                        current_active_pane = current_active_pane.or(active_pane);
+                    }
+                }
+
+                if members.is_empty() {
+                    return None;
                 }
-                (
+
+                Some((
                     Member::Axis(PaneAxis {
                         axis: *axis,
                         members,
                     }),
                     current_active_pane,
-                )
+                ))
             }
             SerializedPaneGroup::Pane(serialized_pane) => {
                 let pane = workspace.update(cx, |workspace, cx| workspace.add_pane(cx));
@@ -123,7 +130,11 @@ impl SerializedPaneGroup {
                     .deserialize_to(project, &pane, workspace_id, workspace, cx)
                     .await;
 
-                (Member::Pane(pane.clone()), active.then(|| pane))
+                if pane.read_with(cx, |pane, _| pane.items().next().is_some()) {
+                    Some((Member::Pane(pane.clone()), active.then(|| pane)))
+                } else {
+                    None
+                }
             }
         }
     }

crates/workspace/src/workspace.rs 🔗

@@ -1244,6 +1244,8 @@ impl Workspace {
             Dock::hide_on_sidebar_shown(self, sidebar_side, cx);
         }
 
+        self.serialize_workspace(cx);
+
         cx.focus_self();
         cx.notify();
     }
@@ -1275,6 +1277,9 @@ impl Workspace {
         } else {
             cx.focus_self();
         }
+
+        self.serialize_workspace(cx);
+
         cx.notify();
     }
 
@@ -1302,6 +1307,9 @@ impl Workspace {
                 cx.focus(active_item.to_any());
             }
         }
+
+        self.serialize_workspace(cx);
+
         cx.notify();
     }
 
@@ -2268,13 +2276,20 @@ impl Workspace {
         self.database_id
     }
 
-    fn location(&self, cx: &AppContext) -> WorkspaceLocation {
-        self.project()
-            .read(cx)
-            .visible_worktrees(cx)
-            .map(|worktree| worktree.read(cx).abs_path())
-            .collect::<Vec<_>>()
-            .into()
+    fn location(&self, cx: &AppContext) -> Option<WorkspaceLocation> {
+        let project = self.project().read(cx);
+
+        if project.is_local() {
+            Some(
+                project
+                    .visible_worktrees(cx)
+                    .map(|worktree| worktree.read(cx).abs_path())
+                    .collect::<Vec<_>>()
+                    .into(),
+            )
+        } else {
+            None
+        }
     }
 
     fn remove_panes(&mut self, member: Member, cx: &mut ViewContext<Workspace>) {
@@ -2331,24 +2346,24 @@ impl Workspace {
             }
         }
 
-        let location = self.location(cx);
-
-        if !location.paths().is_empty() {
-            let dock_pane = serialize_pane_handle(self.dock.pane(), cx);
-            let center_group = build_serialized_pane_group(&self.center.root, cx);
-
-            let serialized_workspace = SerializedWorkspace {
-                id: self.database_id,
-                location: self.location(cx),
-                dock_position: self.dock.position(),
-                dock_pane,
-                center_group,
-                project_panel_open: self.left_sidebar.read(cx).is_open(),
-            };
-
-            cx.background()
-                .spawn(persistence::DB.save_workspace(serialized_workspace))
-                .detach();
+        if let Some(location) = self.location(cx) {
+            if !location.paths().is_empty() {
+                let dock_pane = serialize_pane_handle(self.dock.pane(), cx);
+                let center_group = build_serialized_pane_group(&self.center.root, cx);
+
+                let serialized_workspace = SerializedWorkspace {
+                    id: self.database_id,
+                    location,
+                    dock_position: self.dock.position(),
+                    dock_pane,
+                    center_group,
+                    left_sidebar_open: self.left_sidebar.read(cx).is_open(),
+                };
+
+                cx.background()
+                    .spawn(persistence::DB.save_workspace(serialized_workspace))
+                    .detach();
+            }
         }
     }
 
@@ -2375,34 +2390,46 @@ impl Workspace {
                     .await;
 
                 // Traverse the splits tree and add to things
-                let (root, active_pane) = serialized_workspace
+                let center_group = serialized_workspace
                     .center_group
                     .deserialize(&project, serialized_workspace.id, &workspace, &mut cx)
                     .await;
 
                 // Remove old panes from workspace panes list
                 workspace.update(&mut cx, |workspace, cx| {
-                    workspace.remove_panes(workspace.center.root.clone(), cx);
+                    if let Some((center_group, active_pane)) = center_group {
+                        workspace.remove_panes(workspace.center.root.clone(), cx);
+
+                        // Swap workspace center group
+                        workspace.center = PaneGroup::with_root(center_group);
+
+                        // Change the focus to the workspace first so that we retrigger focus in on the pane.
+                        cx.focus_self();
 
-                    // Swap workspace center group
-                    workspace.center = PaneGroup::with_root(root);
+                        if let Some(active_pane) = active_pane {
+                            cx.focus(active_pane);
+                        } else {
+                            cx.focus(workspace.panes.last().unwrap().clone());
+                        }
+                    } else {
+                        cx.focus_self();
+                    }
 
                     // Note, if this is moved after 'set_dock_position'
                     // it causes an infinite loop.
-                    if serialized_workspace.project_panel_open {
-                        workspace.toggle_sidebar_item_focus(SidebarSide::Left, 0, cx)
+                    if workspace.left_sidebar().read(cx).is_open()
+                        != serialized_workspace.left_sidebar_open
+                    {
+                        workspace.toggle_sidebar(SidebarSide::Left, cx);
                     }
 
-                    Dock::set_dock_position(workspace, serialized_workspace.dock_position, cx);
-
-                    if let Some(active_pane) = active_pane {
-                        // Change the focus to the workspace first so that we retrigger focus in on the pane.
-                        cx.focus_self();
-                        cx.focus(active_pane);
-                    }
+                    // Dock::set_dock_position(workspace, serialized_workspace.dock_position, cx);
 
                     cx.notify();
                 });
+
+                // Serialize ourself to make sure our timestamps and any pane / item changes are replicated
+                workspace.read_with(&cx, |workspace, cx| workspace.serialize_workspace(cx))
             }
         })
         .detach();

crates/zed/src/zed.rs 🔗

@@ -377,7 +377,7 @@ fn quit(_: &Quit, cx: &mut gpui::MutableAppContext) {
 }
 
 fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext<Workspace>) {
-    let app_name = cx.global::<ReleaseChannel>().name();
+    let app_name = cx.global::<ReleaseChannel>().display_name();
     let version = env!("CARGO_PKG_VERSION");
     cx.prompt(
         gpui::PromptLevel::Info,