Added UUID based, stable workspace ID for caching on item startup. Completed first sketch of terminal persistence. Still need to debug it though....

Mikayla Maki created

Change summary

Cargo.lock                                     |   2 
crates/db/Cargo.toml                           |   1 
crates/db/src/db.rs                            | 141 +++++++
crates/diagnostics/src/diagnostics.rs          |   6 
crates/editor/src/editor.rs                    |  50 +-
crates/editor/src/items.rs                     |   5 
crates/editor/src/persistence.rs               |  18 
crates/search/src/project_search.rs            |   4 
crates/sqlez/src/bindable.rs                   |   7 
crates/sqlez/src/typed_statements.rs           |  54 ---
crates/terminal/src/persistence.rs             |  20 
crates/terminal/src/terminal.rs                |  35 +
crates/terminal/src/terminal_container_view.rs |  40 +-
crates/workspace/src/dock.rs                   |   2 
crates/workspace/src/item.rs                   |  31 +
crates/workspace/src/persistence.rs            | 345 ++++++++++---------
crates/workspace/src/persistence/model.rs      |  49 +
crates/workspace/src/shared_screen.rs          |  10 
crates/workspace/src/workspace.rs              |  43 +
crates/zed/src/main.rs                         |   4 
20 files changed, 502 insertions(+), 365 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1572,6 +1572,7 @@ dependencies = [
  "sqlez",
  "tempdir",
  "util",
+ "uuid 1.2.2",
 ]
 
 [[package]]
@@ -6834,6 +6835,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "422ee0de9031b5b948b97a8fc04e3aa35230001a722ddd27943e0be31564ce4c"
 dependencies = [
  "getrandom 0.2.8",
+ "rand 0.8.5",
 ]
 
 [[package]]

crates/db/Cargo.toml 🔗

@@ -22,6 +22,7 @@ lazy_static = "1.4.0"
 log = { version = "0.4.16", features = ["kv_unstable_serde"] }
 parking_lot = "0.11.1"
 serde = { version = "1.0", features = ["derive"] }
+uuid = { version = "1.2.2", features = ["v4", "fast-rng"] }
 
 [dev-dependencies]
 gpui = { path = "../gpui", features = ["test-support"] }

crates/db/src/db.rs 🔗

@@ -1,21 +1,26 @@
 pub mod kvp;
 
-// Re-export indoc and sqlez so clients only need to include us
+// Re-export
+pub use anyhow;
 pub use indoc::indoc;
 pub use lazy_static;
 pub use sqlez;
-
-use std::fs::{create_dir_all, remove_dir_all};
-use std::path::Path;
+use sqlez::bindable::{Bind, Column};
 
 #[cfg(any(test, feature = "test-support"))]
 use anyhow::Result;
 #[cfg(any(test, feature = "test-support"))]
 use sqlez::connection::Connection;
-use sqlez::domain::{Domain, Migrator};
+#[cfg(any(test, feature = "test-support"))]
+use sqlez::domain::Domain;
+
+use sqlez::domain::Migrator;
 use sqlez::thread_safe_connection::ThreadSafeConnection;
+use std::fs::{create_dir_all, remove_dir_all};
+use std::path::Path;
 use util::channel::{ReleaseChannel, RELEASE_CHANNEL, RELEASE_CHANNEL_NAME};
 use util::paths::DB_DIR;
+use uuid::Uuid as RealUuid;
 
 const INITIALIZE_QUERY: &'static str = indoc! {"
     PRAGMA journal_mode=WAL;
@@ -25,6 +30,47 @@ const INITIALIZE_QUERY: &'static str = indoc! {"
     PRAGMA case_sensitive_like=TRUE;
 "};
 
+#[derive(Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
+pub struct Uuid(RealUuid);
+
+impl std::ops::Deref for Uuid {
+    type Target = RealUuid;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+impl Bind for Uuid {
+    fn bind(
+        &self,
+        statement: &sqlez::statement::Statement,
+        start_index: i32,
+    ) -> anyhow::Result<i32> {
+        statement.bind(self.as_bytes(), start_index)
+    }
+}
+
+impl Column for Uuid {
+    fn column(
+        statement: &mut sqlez::statement::Statement,
+        start_index: i32,
+    ) -> anyhow::Result<(Self, i32)> {
+        let blob = statement.column_blob(start_index)?;
+        Ok((Uuid::from_bytes(blob)?, start_index + 1))
+    }
+}
+
+impl Uuid {
+    pub fn new() -> Self {
+        Uuid(RealUuid::new_v4())
+    }
+
+    fn from_bytes(bytes: &[u8]) -> anyhow::Result<Self> {
+        Ok(Uuid(RealUuid::from_bytes(bytes.try_into()?)))
+    }
+}
+
 /// Open or create a database at the given directory path.
 pub fn open_file_db<M: Migrator>() -> ThreadSafeConnection<M> {
     // Use 0 for now. Will implement incrementing and clearing of old db files soon TM
@@ -77,3 +123,88 @@ macro_rules! connection {
         }
     };
 }
+
+#[macro_export]
+macro_rules! exec_method {
+    ($id:ident(): $sql:literal) => {
+         pub fn $id(&self) -> $crate::sqlez::anyhow::Result<()> {
+             use $crate::anyhow::Context;
+
+             self.exec($sql)?()
+                 .context(::std::format!(
+                     "Error in {}, exec failed to execute or parse for: {}",
+                     ::std::stringify!($id),
+                     ::std::stringify!($sql),
+                 ))
+         }
+    };
+    ($id:ident($($arg:ident: $arg_type:ty),+): $sql:literal) => {
+         pub fn $id(&self, $($arg: $arg_type),+) -> $crate::sqlez::anyhow::Result<()> {
+             use $crate::anyhow::Context;
+
+             self.exec_bound::<($($arg_type),+)>($sql)?(($($arg),+))
+                 .context(::std::format!(
+                     "Error in {}, exec_bound failed to execute or parse for: {}",
+                     ::std::stringify!($id),
+                     ::std::stringify!($sql),
+                 ))
+         }
+    };
+}
+
+#[macro_export]
+macro_rules! select_method {
+    ($id:ident() ->  $return_type:ty: $sql:literal) => {
+         pub fn $id(&self) -> $crate::sqlez::anyhow::Result<Vec<$return_type>> {
+             use $crate::anyhow::Context;
+
+             self.select::<$return_type>($sql)?(())
+                 .context(::std::format!(
+                     "Error in {}, select_row failed to execute or parse for: {}",
+                     ::std::stringify!($id),
+                     ::std::stringify!($sql),
+                 ))
+         }
+    };
+    ($id:ident($($arg:ident: $arg_type:ty),+) -> $return_type:ty: $sql:literal) => {
+         pub fn $id(&self, $($arg: $arg_type),+) -> $crate::sqlez::anyhow::Result<Vec<$return_type>> {
+             use $crate::anyhow::Context;
+
+             self.select_bound::<($($arg_type),+), $return_type>($sql)?(($($arg),+))
+                 .context(::std::format!(
+                     "Error in {}, exec_bound failed to execute or parse for: {}",
+                     ::std::stringify!($id),
+                     ::std::stringify!($sql),
+                 ))
+         }
+    };
+}
+
+#[macro_export]
+macro_rules! select_row_method {
+    ($id:ident() ->  $return_type:ty: $sql:literal) => {
+         pub fn $id(&self) -> $crate::sqlez::anyhow::Result<Option<$return_type>> {
+             use $crate::anyhow::Context;
+
+             self.select_row::<$return_type>($sql)?(())
+                 .context(::std::format!(
+                     "Error in {}, select_row failed to execute or parse for: {}",
+                     ::std::stringify!($id),
+                     ::std::stringify!($sql),
+                 ))
+         }
+    };
+    ($id:ident($($arg:ident: $arg_type:ty),+) ->  $return_type:ty: $sql:literal) => {
+         pub fn $id(&self, $($arg: $arg_type),+) -> $crate::sqlez::anyhow::Result<Option<$return_type>>  {
+             use $crate::anyhow::Context;
+
+             self.select_row_bound::<($($arg_type),+), $return_type>($sql)?(($($arg),+))
+                 .context(::std::format!(
+                     "Error in {}, select_row_bound failed to execute or parse for: {}",
+                     ::std::stringify!($id),
+                     ::std::stringify!($sql),
+                 ))
+
+         }
+    };
+}

crates/diagnostics/src/diagnostics.rs 🔗

@@ -584,7 +584,11 @@ impl Item for ProjectDiagnosticsEditor {
         });
     }
 
-    fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
+    fn clone_on_split(
+        &self,
+        _workspace_id: workspace::WorkspaceId,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Self>
     where
         Self: Sized,
     {

crates/editor/src/editor.rs 🔗

@@ -83,7 +83,7 @@ use theme::{DiagnosticStyle, Theme};
 use util::{post_inc, ResultExt, TryFutureExt};
 use workspace::{ItemNavHistory, Workspace};
 
-use crate::{git::diff_hunk_to_display, persistence::DB};
+use crate::git::diff_hunk_to_display;
 
 const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
 const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
@@ -1137,30 +1137,30 @@ impl Editor {
         cx: &mut ViewContext<Self>,
     ) -> Self {
         let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx));
-        if let Some(project) = project.as_ref() {
-            if let Some(file) = buffer
-                .read(cx)
-                .as_singleton()
-                .and_then(|buffer| buffer.read(cx).file())
-                .and_then(|file| file.as_local())
-            {
-                let item_id = cx.weak_handle().id();
-                let workspace_id = project
-                    .read(cx)
-                    .visible_worktrees(cx)
-                    .map(|worktree| worktree.read(cx).abs_path())
-                    .collect::<Vec<_>>()
-                    .into();
-                let path = file.abs_path(cx);
-                dbg!(&path);
-
-                cx.background()
-                    .spawn(async move {
-                        DB.save_path(item_id, workspace_id, path).log_err();
-                    })
-                    .detach();
-            }
-        }
+        // if let Some(project) = project.as_ref() {
+        //     if let Some(file) = buffer
+        //         .read(cx)
+        //         .as_singleton()
+        //         .and_then(|buffer| buffer.read(cx).file())
+        //         .and_then(|file| file.as_local())
+        //     {
+        //         // let item_id = cx.weak_handle().id();
+        //         // let workspace_id = project
+        //         //     .read(cx)
+        //         //     .visible_worktrees(cx)
+        //         //     .map(|worktree| worktree.read(cx).abs_path())
+        //         //     .collect::<Vec<_>>()
+        //         //     .into();
+        //         let path = file.abs_path(cx);
+        //         dbg!(&path);
+
+        //         // cx.background()
+        //         //     .spawn(async move {
+        //         //         DB.save_path(item_id, workspace_id, path).log_err();
+        //         //     })
+        //         //     .detach();
+        //     }
+        // }
 
         Self::new(EditorMode::Full, buffer, project, None, cx)
     }

crates/editor/src/items.rs 🔗

@@ -368,7 +368,7 @@ impl Item for Editor {
         self.buffer.read(cx).is_singleton()
     }
 
-    fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
+    fn clone_on_split(&self, _workspace_id: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<Self>
     where
         Self: Sized,
     {
@@ -561,14 +561,13 @@ impl Item for Editor {
     fn deserialize(
         project: ModelHandle<Project>,
         _workspace: WeakViewHandle<Workspace>,
-        workspace_id: WorkspaceId,
+        workspace_id: workspace::WorkspaceId,
         item_id: ItemId,
         cx: &mut ViewContext<Pane>,
     ) -> Task<Result<ViewHandle<Self>>> {
         if let Some(project_item) = project.update(cx, |project, cx| {
             // Look up the path with this key associated, create a self with that path
             let path = DB.get_path(item_id, workspace_id).ok()?;
-            dbg!(&path);
             let (worktree, path) = project.find_local_worktree(&path, cx)?;
             let project_path = ProjectPath {
                 worktree_id: worktree.read(cx).id(),

crates/editor/src/persistence.rs 🔗

@@ -1,7 +1,7 @@
 use std::path::{Path, PathBuf};
 
 use anyhow::{Context, Result};
-use db::connection;
+use db::{connection, exec_method};
 use indoc::indoc;
 use sqlez::domain::Domain;
 use workspace::{ItemId, Workspace, WorkspaceId};
@@ -35,18 +35,12 @@ impl EditorDb {
     pub fn get_path(&self, item_id: ItemId, workspace_id: WorkspaceId) -> Result<PathBuf> {
         self.select_row_bound(indoc! {"
             SELECT path FROM editors 
-            WHERE item_id = ? AND workspace_id = ?"})?((item_id, &workspace_id))?
+            WHERE item_id = ? AND workspace_id = ?"})?((item_id, workspace_id))?
         .context("Path not found for serialized editor")
     }
 
-    pub fn save_path(
-        &self,
-        item_id: ItemId,
-        workspace_id: WorkspaceId,
-        path: PathBuf,
-    ) -> Result<()> {
-        self.exec_bound::<(ItemId, &WorkspaceId, &Path)>(indoc! {"
-            INSERT OR REPLACE INTO editors(item_id, workspace_id, path)
-            VALUES (?, ?, ?)"})?((item_id, &workspace_id, &path))
-    }
+    exec_method!(save_path(item_id: ItemId, workspace_id: WorkspaceId, path: &Path):
+        "INSERT OR REPLACE INTO editors(item_id, workspace_id, path)
+         VALUES (?, ?, ?)"
+    );
 }

crates/search/src/project_search.rs 🔗

@@ -26,7 +26,7 @@ use util::ResultExt as _;
 use workspace::{
     item::{Item, ItemEvent, ItemHandle},
     searchable::{Direction, SearchableItem, SearchableItemHandle},
-    ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, Workspace,
+    ItemNavHistory, Pane, ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceId,
 };
 
 actions!(project_search, [SearchInNew, ToggleFocus]);
@@ -315,7 +315,7 @@ impl Item for ProjectSearchView {
             .update(cx, |editor, cx| editor.reload(project, cx))
     }
 
-    fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
+    fn clone_on_split(&self, _workspace_id: WorkspaceId, cx: &mut ViewContext<Self>) -> Option<Self>
     where
         Self: Sized,
     {

crates/sqlez/src/bindable.rs 🔗

@@ -36,6 +36,13 @@ impl Bind for &[u8] {
     }
 }
 
+impl<const C: usize> Bind for &[u8; C] {
+    fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
+        statement.bind_blob(start_index, self.as_slice())?;
+        Ok(start_index + 1)
+    }
+}
+
 impl Bind for Vec<u8> {
     fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
         statement.bind_blob(start_index, self)?;

crates/sqlez/src/typed_statements.rs 🔗

@@ -52,57 +52,3 @@ impl Connection {
         Ok(move |bindings| statement.with_bindings(bindings)?.maybe_row::<C>())
     }
 }
-
-#[macro_export]
-macro_rules! exec_method {
-    ($id:ident(): $sql:literal) => {
-         pub fn $id(&self) -> $crate::anyhow::Result<()> {
-             iife!({
-                 self.exec($sql)?()
-             })
-         }
-    };
-    ($id:ident($($arg:ident: $arg_type:ty),+): $sql:literal) => {
-         pub fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<()> {
-             iife!({
-                 self.exec_bound::<($($arg_type),+)>($sql)?(($($arg),+))
-             })
-         }
-    };
-}
-
-#[macro_export]
-macro_rules! select_method {
-    ($id:ident() ->  $return_type:ty: $sql:literal) => {
-         pub fn $id(&self) -> $crate::anyhow::Result<Vec<$return_type>> {
-             iife!({
-                 self.select::<$return_type>($sql)?(())
-             })
-         }
-    };
-    ($id:ident($($arg:ident: $arg_type:ty),+) -> $return_type:ty: $sql:literal) => {
-         pub fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<Vec<$return_type>> {
-             iife!({
-                 self.exec_bound::<($($arg_type),+), $return_type>($sql)?(($($arg),+))
-             })
-         }
-    };
-}
-
-#[macro_export]
-macro_rules! select_row_method {
-    ($id:ident() ->  $return_type:ty: $sql:literal) => {
-         pub fn $id(&self) -> $crate::anyhow::Result<Option<$return_type>> {
-             iife!({
-                 self.select_row::<$return_type>($sql)?(())
-             })
-         }
-    };
-    ($id:ident($($arg:ident: $arg_type:ty),+) ->  $return_type:ty: $sql:literal) => {
-         pub fn $id(&self, $($arg: $arg_type),+) -> $crate::anyhow::Result<Option<$return_type>>  {
-             iife!({
-                 self.select_row_bound::<($($arg_type),+), $return_type>($sql)?(($($arg),+))
-             })
-         }
-    };
-}

crates/terminal/src/persistence.rs 🔗

@@ -1,10 +1,7 @@
 use std::path::{Path, PathBuf};
 
-use db::{
-    connection, indoc,
-    sqlez::{domain::Domain, exec_method, select_row_method},
-};
-use util::iife;
+use db::{connection, exec_method, indoc, select_row_method, sqlez::domain::Domain};
+
 use workspace::{ItemId, Workspace, WorkspaceId};
 
 use crate::Terminal;
@@ -19,13 +16,12 @@ impl Domain for Terminal {
     fn migrations() -> &'static [&'static str] {
         &[indoc! {"
             CREATE TABLE terminals (
-                item_id INTEGER,
                 workspace_id BLOB,
+                item_id INTEGER,
                 working_directory BLOB,
-                PRIMARY KEY(item_id, workspace_id),
+                PRIMARY KEY(workspace_id, item_id),
                 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
                     ON DELETE CASCADE
-                    ON UPDATE CASCADE
             ) STRICT;
         "}]
     }
@@ -33,15 +29,15 @@ impl Domain for Terminal {
 
 impl TerminalDb {
     exec_method!(
-        save_working_directory(item_id: ItemId, workspace_id: &WorkspaceId, working_directory: &Path):
+        save_working_directory(model_id: ItemId, workspace_id: WorkspaceId, working_directory: &Path):
             "INSERT OR REPLACE INTO terminals(item_id, workspace_id, working_directory)
-             VALUES (?, ?, ?)"
+             VALUES (?1, ?2, ?3)"
     );
 
     select_row_method!(
-        get_working_directory(item_id: ItemId, workspace_id: &WorkspaceId) -> PathBuf:
+        get_working_directory(item_id: ItemId, workspace_id: WorkspaceId) -> PathBuf:
             "SELECT working_directory
              FROM terminals 
-             WHERE item_id = ? workspace_id = ?"
+             WHERE item_id = ? AND workspace_id = ?"
     );
 }

crates/terminal/src/terminal.rs 🔗

@@ -33,9 +33,11 @@ use mappings::mouse::{
     alt_scroll, grid_point, mouse_button_report, mouse_moved_report, mouse_side, scroll_report,
 };
 
+use persistence::TERMINAL_CONNECTION;
 use procinfo::LocalProcessInfo;
 use settings::{AlternateScroll, Settings, Shell, TerminalBlink};
 use util::ResultExt;
+use workspace::{ItemId, WorkspaceId};
 
 use std::{
     cmp::min,
@@ -282,6 +284,8 @@ impl TerminalBuilder {
         blink_settings: Option<TerminalBlink>,
         alternate_scroll: &AlternateScroll,
         window_id: usize,
+        item_id: ItemId,
+        workspace_id: WorkspaceId,
     ) -> Result<TerminalBuilder> {
         let pty_config = {
             let alac_shell = shell.clone().and_then(|shell| match shell {
@@ -386,6 +390,8 @@ impl TerminalBuilder {
             last_mouse_position: None,
             next_link_id: 0,
             selection_phase: SelectionPhase::Ended,
+            workspace_id,
+            item_id,
         };
 
         Ok(TerminalBuilder {
@@ -529,6 +535,8 @@ pub struct Terminal {
     scroll_px: f32,
     next_link_id: usize,
     selection_phase: SelectionPhase,
+    workspace_id: WorkspaceId,
+    item_id: ItemId,
 }
 
 impl Terminal {
@@ -566,20 +574,6 @@ impl Terminal {
             }
             AlacTermEvent::Wakeup => {
                 cx.emit(Event::Wakeup);
-
-                if self.update_process_info() {
-                    cx.emit(Event::TitleChanged);
-
-                    // if let Some(foreground_info) = self.foreground_process_info {
-                    // cx.background().spawn(async move {
-                    //     TERMINAL_CONNECTION.save_working_directory(
-                    //         self.item_id,
-                    //         &self.workspace_id,
-                    //         &foreground_info.cwd,
-                    //     );
-                    // });
-                    // }
-                }
             }
             AlacTermEvent::ColorRequest(idx, fun_ptr) => {
                 self.events
@@ -888,6 +882,19 @@ impl Terminal {
 
         if self.update_process_info() {
             cx.emit(Event::TitleChanged);
+
+            if let Some(foreground_info) = &self.foreground_process_info {
+                let cwd = foreground_info.cwd.clone();
+                let item_id = self.item_id;
+                let workspace_id = self.workspace_id;
+                cx.background()
+                    .spawn(async move {
+                        TERMINAL_CONNECTION
+                            .save_working_directory(item_id, workspace_id, cwd.as_path())
+                            .log_err();
+                    })
+                    .detach();
+            }
         }
 
         //Note that the ordering of events matters for event processing

crates/terminal/src/terminal_container_view.rs 🔗

@@ -1,6 +1,6 @@
 use crate::persistence::TERMINAL_CONNECTION;
 use crate::terminal_view::TerminalView;
-use crate::{Event, Terminal, TerminalBuilder, TerminalError};
+use crate::{Event, TerminalBuilder, TerminalError};
 
 use alacritty_terminal::index::Point;
 use dirs::home_dir;
@@ -14,7 +14,7 @@ use workspace::{
     item::{Item, ItemEvent},
     ToolbarItemLocation, Workspace,
 };
-use workspace::{register_deserializable_item, Pane};
+use workspace::{register_deserializable_item, Pane, WorkspaceId};
 
 use project::{LocalWorktree, Project, ProjectPath};
 use settings::{AlternateScroll, Settings, WorkingDirectory};
@@ -82,7 +82,9 @@ impl TerminalContainer {
             .unwrap_or(WorkingDirectory::CurrentProjectDirectory);
 
         let working_directory = get_working_directory(workspace, cx, strategy);
-        let view = cx.add_view(|cx| TerminalContainer::new(working_directory, false, cx));
+        let view = cx.add_view(|cx| {
+            TerminalContainer::new(working_directory, false, workspace.database_id(), cx)
+        });
         workspace.add_item(Box::new(view), cx);
     }
 
@@ -90,6 +92,7 @@ impl TerminalContainer {
     pub fn new(
         working_directory: Option<PathBuf>,
         modal: bool,
+        workspace_id: WorkspaceId,
         cx: &mut ViewContext<Self>,
     ) -> Self {
         let settings = cx.global::<Settings>();
@@ -116,10 +119,13 @@ impl TerminalContainer {
             settings.terminal_overrides.blinking.clone(),
             scroll,
             cx.window_id(),
+            cx.view_id(),
+            workspace_id,
         ) {
             Ok(terminal) => {
                 let terminal = cx.add_model(|cx| terminal.subscribe(cx));
                 let view = cx.add_view(|cx| TerminalView::from_terminal(terminal, modal, cx));
+
                 cx.subscribe(&view, |_this, _content, event, cx| cx.emit(*event))
                     .detach();
                 TerminalContainerContent::Connected(view)
@@ -139,18 +145,6 @@ impl TerminalContainer {
         }
     }
 
-    pub fn from_terminal(
-        terminal: ModelHandle<Terminal>,
-        modal: bool,
-        cx: &mut ViewContext<Self>,
-    ) -> Self {
-        let connected_view = cx.add_view(|cx| TerminalView::from_terminal(terminal, modal, cx));
-        TerminalContainer {
-            content: TerminalContainerContent::Connected(connected_view),
-            associated_directory: None,
-        }
-    }
-
     fn connected(&self) -> Option<ViewHandle<TerminalView>> {
         match &self.content {
             TerminalContainerContent::Connected(vh) => Some(vh.clone()),
@@ -278,13 +272,18 @@ impl Item for TerminalContainer {
             .boxed()
     }
 
-    fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self> {
+    fn clone_on_split(
+        &self,
+        workspace_id: WorkspaceId,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Self> {
         //From what I can tell, there's no  way to tell the current working
         //Directory of the terminal from outside the shell. There might be
         //solutions to this, but they are non-trivial and require more IPC
         Some(TerminalContainer::new(
             self.associated_directory.clone(),
             false,
+            workspace_id,
             cx,
         ))
     }
@@ -391,9 +390,14 @@ impl Item for TerminalContainer {
         item_id: workspace::ItemId,
         cx: &mut ViewContext<Pane>,
     ) -> Task<anyhow::Result<ViewHandle<Self>>> {
-        let working_directory = TERMINAL_CONNECTION.get_working_directory(item_id, &workspace_id);
+        let working_directory = TERMINAL_CONNECTION.get_working_directory(item_id, workspace_id);
         Task::ready(Ok(cx.add_view(|cx| {
-            TerminalContainer::new(working_directory.log_err().flatten(), false, cx)
+            TerminalContainer::new(
+                working_directory.log_err().flatten(),
+                false,
+                workspace_id,
+                cx,
+            )
         })))
     }
 }

crates/workspace/src/dock.rs 🔗

@@ -206,7 +206,7 @@ impl Dock {
             cx.focus(last_active_center_pane);
         }
         cx.emit(crate::Event::DockAnchorChanged);
-        workspace.serialize_workspace(None, cx);
+        workspace.serialize_workspace(cx);
         cx.notify();
     }
 

crates/workspace/src/item.rs 🔗

@@ -22,11 +22,8 @@ use theme::Theme;
 use util::ResultExt;
 
 use crate::{
-    pane,
-    persistence::model::{ItemId, WorkspaceId},
-    searchable::SearchableItemHandle,
-    DelayedDebouncedEditAction, FollowableItemBuilders, ItemNavHistory, Pane, ToolbarItemLocation,
-    Workspace,
+    pane, persistence::model::ItemId, searchable::SearchableItemHandle, DelayedDebouncedEditAction,
+    FollowableItemBuilders, ItemNavHistory, Pane, ToolbarItemLocation, Workspace, WorkspaceId,
 };
 
 #[derive(Eq, PartialEq, Hash)]
@@ -52,7 +49,7 @@ pub trait Item: View {
     fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
     fn is_singleton(&self, cx: &AppContext) -> bool;
     fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext<Self>);
-    fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
+    fn clone_on_split(&self, _workspace_id: WorkspaceId, _: &mut ViewContext<Self>) -> Option<Self>
     where
         Self: Sized,
     {
@@ -121,7 +118,9 @@ pub trait Item: View {
     fn breadcrumbs(&self, _theme: &Theme, _cx: &AppContext) -> Option<Vec<ElementBox>> {
         None
     }
+
     fn serialized_item_kind() -> Option<&'static str>;
+
     fn deserialize(
         project: ModelHandle<Project>,
         workspace: WeakViewHandle<Workspace>,
@@ -144,7 +143,11 @@ pub trait ItemHandle: 'static + fmt::Debug {
     fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
     fn is_singleton(&self, cx: &AppContext) -> bool;
     fn boxed_clone(&self) -> Box<dyn ItemHandle>;
-    fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemHandle>>;
+    fn clone_on_split(
+        &self,
+        workspace_id: WorkspaceId,
+        cx: &mut MutableAppContext,
+    ) -> Option<Box<dyn ItemHandle>>;
     fn added_to_pane(
         &self,
         workspace: &mut Workspace,
@@ -246,9 +249,13 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
         Box::new(self.clone())
     }
 
-    fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemHandle>> {
+    fn clone_on_split(
+        &self,
+        workspace_id: WorkspaceId,
+        cx: &mut MutableAppContext,
+    ) -> Option<Box<dyn ItemHandle>> {
         self.update(cx, |item, cx| {
-            cx.add_option_view(|cx| item.clone_on_split(cx))
+            cx.add_option_view(|cx| item.clone_on_split(workspace_id, cx))
         })
         .map(|handle| Box::new(handle) as Box<dyn ItemHandle>)
     }
@@ -812,7 +819,11 @@ pub(crate) mod test {
             self.push_to_nav_history(cx);
         }
 
-        fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
+        fn clone_on_split(
+            &self,
+            _workspace_id: WorkspaceId,
+            _: &mut ViewContext<Self>,
+        ) -> Option<Self>
         where
             Self: Sized,
         {

crates/workspace/src/persistence.rs 🔗

@@ -2,39 +2,38 @@
 
 pub mod model;
 
-use std::path::{Path, PathBuf};
-use std::sync::Arc;
+use std::path::Path;
 
-use anyhow::{anyhow, bail, Result, Context};
+use anyhow::{anyhow, bail, Context, Result};
 use db::connection;
 use gpui::Axis;
 use indoc::indoc;
 
-
 use db::sqlez::domain::Domain;
 use util::{iife, unzip_option, ResultExt};
 
 use crate::dock::DockPosition;
+use crate::WorkspaceId;
 
 use super::Workspace;
 
 use model::{
-    GroupId, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup,
-    SerializedWorkspace, WorkspaceId,
+    GroupId, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
+    WorkspaceLocation,
 };
 
 connection!(DB: WorkspaceDb<Workspace>);
 
-
 impl Domain for Workspace {
     fn name() -> &'static str {
         "workspace"
     }
-    
+
     fn migrations() -> &'static [&'static str] {
         &[indoc! {"
             CREATE TABLE workspaces(
                 workspace_id BLOB PRIMARY KEY,
+                workspace_location BLOB NOT NULL UNIQUE,
                 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
@@ -97,21 +96,25 @@ impl WorkspaceDb {
         &self,
         worktree_roots: &[P],
     ) -> Option<SerializedWorkspace> {
-        let workspace_id: WorkspaceId = worktree_roots.into();
+        let workspace_location: WorkspaceLocation = worktree_roots.into();
 
         // 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, dock_position): (WorkspaceId, DockPosition) = iife!({
+        let (workspace_id, workspace_location, dock_position): (
+            WorkspaceId,
+            WorkspaceLocation,
+            DockPosition,
+        ) = iife!({
             if worktree_roots.len() == 0 {
                 self.select_row(indoc! {"
-                    SELECT workspace_id, dock_visible, dock_anchor
+                    SELECT workspace_id, workspace_location, dock_visible, dock_anchor
                     FROM workspaces 
                     ORDER BY timestamp DESC LIMIT 1"})?()?
             } else {
                 self.select_row_bound(indoc! {"
-                    SELECT workspace_id, dock_visible, dock_anchor
+                    SELECT workspace_id, workspace_location, dock_visible, dock_anchor
                     FROM workspaces 
-                    WHERE workspace_id = ?"})?(&workspace_id)?
+                    WHERE workspace_location = ?"})?(&workspace_location)?
             }
             .context("No workspaces found")
         })
@@ -119,13 +122,14 @@ impl WorkspaceDb {
         .flatten()?;
 
         Some(SerializedWorkspace {
-            workspace_id: workspace_id.clone(),
+            id: workspace_id,
+            location: workspace_location.clone(),
             dock_pane: self
-                .get_dock_pane(&workspace_id)
+                .get_dock_pane(workspace_id)
                 .context("Getting dock pane")
                 .log_err()?,
             center_group: self
-                .get_center_pane_group(&workspace_id)
+                .get_center_pane_group(workspace_id)
                 .context("Getting center group")
                 .log_err()?,
             dock_position,
@@ -134,72 +138,61 @@ impl WorkspaceDb {
 
     /// Saves a workspace using the worktree roots. Will garbage collect any workspaces
     /// that used this workspace previously
-    pub fn save_workspace(
-        &self,
-        old_id: Option<WorkspaceId>,
-        workspace: &SerializedWorkspace,
-    ) {
+    pub fn save_workspace(&self, workspace: &SerializedWorkspace) {
         self.with_savepoint("update_worktrees", || {
+            // Clear out panes and pane_groups
             self.exec_bound(indoc! {"
                 UPDATE workspaces SET dock_pane = NULL WHERE workspace_id = ?1;
                 DELETE FROM pane_groups WHERE workspace_id = ?1;
-                DELETE FROM panes WHERE workspace_id = ?1;"})?
-            (old_id.as_ref().unwrap_or(&workspace.workspace_id)).context("Clearing old panes")?;
-            
-            if let Some(old_id) = old_id {
-                self.exec_bound(indoc! {"
-                    UPDATE OR REPLACE workspaces
-                    SET workspace_id = ?,
-                        dock_visible = ?,
-                        dock_anchor = ?,
-                        timestamp = CURRENT_TIMESTAMP
-                    WHERE workspace_id = ?"})?((
-                    &workspace.workspace_id,
-                    workspace.dock_position,
-                    &old_id,
-                )).context("Updating workspace with new worktree roots")?;
-            } else {
-                self.exec_bound(
-                    "INSERT OR REPLACE INTO workspaces(workspace_id, dock_visible, dock_anchor) VALUES (?, ?, ?)",
-                )?((&workspace.workspace_id, workspace.dock_position)).context("Uodating workspace")?;
-            }
-            
+                DELETE FROM panes WHERE workspace_id = ?1;"})?(workspace.id)
+            .context("Clearing old panes")?;
+
+            // Update or insert
+            self.exec_bound(indoc! {
+                "INSERT OR REPLACE INTO
+                    workspaces(workspace_id, workspace_location, dock_visible, dock_anchor, timestamp) 
+                 VALUES 
+                    (?1, ?2, ?3, ?4, CURRENT_TIMESTAMP)"
+            })?((workspace.id, &workspace.location, workspace.dock_position))
+            .context("Updating workspace")?;
+
             // Save center pane group and dock pane
-            self.save_pane_group(&workspace.workspace_id, &workspace.center_group, None).context("save pane group in save workspace")?;
-            
-            let dock_id = self.save_pane(&workspace.workspace_id, &workspace.dock_pane, None, true).context("save pane in save workspace")?;
-        
+            self.save_pane_group(workspace.id, &workspace.center_group, None)
+                .context("save pane group in save workspace")?;
+
+            let dock_id = self
+                .save_pane(workspace.id, &workspace.dock_pane, None, true)
+                .context("save pane in save workspace")?;
+
             // Complete workspace initialization
             self.exec_bound(indoc! {"
                 UPDATE workspaces
                 SET dock_pane = ?
-                WHERE workspace_id = ?"})?((
-                dock_id,
-                &workspace.workspace_id,
-            )).context("Finishing initialization with dock pane")?;
+                WHERE workspace_id = ?"})?((dock_id, workspace.id))
+            .context("Finishing initialization with dock pane")?;
 
             Ok(())
         })
         .with_context(|| {
             format!(
-                "Update workspace with roots {:?} failed.",
-                workspace.workspace_id.paths()
+                "Update workspace with roots {:?} and id {:?} failed.",
+                workspace.location.paths(),
+                workspace.id
             )
         })
         .log_err();
     }
 
     /// Returns the previous workspace ids sorted by last modified along with their opened worktree roots
-    pub fn recent_workspaces(&self, limit: usize) -> Vec<Arc<Vec<PathBuf>>> {
+    pub fn recent_workspaces(&self, limit: usize) -> Vec<(WorkspaceId, WorkspaceLocation)> {
         iife!({
             // TODO, upgrade anyhow: https://docs.rs/anyhow/1.0.66/anyhow/fn.Ok.html
             Ok::<_, anyhow::Error>(
-                self.select_bound::<usize, WorkspaceId>(
-                    "SELECT workspace_id FROM workspaces ORDER BY timestamp DESC LIMIT ?",
+                self.select_bound::<usize, (WorkspaceId, WorkspaceLocation)>(
+                    "SELECT workspace_id, workspace_location FROM workspaces ORDER BY timestamp DESC LIMIT ?",
                 )?(limit)?
                 .into_iter()
-                .map(|id| id.paths())
-                .collect::<Vec<Arc<Vec<PathBuf>>>>(),
+                .collect::<Vec<(WorkspaceId, WorkspaceLocation)>>(),
             )
         })
         .log_err()
@@ -208,7 +201,7 @@ impl WorkspaceDb {
 
     pub(crate) fn get_center_pane_group(
         &self,
-        workspace_id: &WorkspaceId,
+        workspace_id: WorkspaceId,
     ) -> Result<SerializedPaneGroup> {
         self.get_pane_group(workspace_id, None)?
             .into_iter()
@@ -218,10 +211,10 @@ impl WorkspaceDb {
 
     fn get_pane_group(
         &self,
-        workspace_id: &WorkspaceId,
+        workspace_id: WorkspaceId,
         group_id: Option<GroupId>,
     ) -> Result<Vec<SerializedPaneGroup>> {
-        type GroupKey<'a> = (Option<GroupId>, &'a WorkspaceId);
+        type GroupKey = (Option<GroupId>, WorkspaceId);
         type GroupOrPane = (Option<GroupId>, Option<Axis>, Option<PaneId>, Option<bool>);
         self.select_bound::<GroupKey, GroupOrPane>(indoc! {"
             SELECT group_id, axis, pane_id, active
@@ -253,31 +246,29 @@ impl WorkspaceDb {
             if let Some((group_id, axis)) = group_id.zip(axis) {
                 Ok(SerializedPaneGroup::Group {
                     axis,
-                    children: self.get_pane_group(
-                        workspace_id,
-                        Some(group_id),
-                    )?,
+                    children: self.get_pane_group(workspace_id, Some(group_id))?,
                 })
             } else if let Some((pane_id, active)) = pane_id.zip(active) {
-                Ok(SerializedPaneGroup::Pane(SerializedPane::new(self.get_items( pane_id)?, active)))
+                Ok(SerializedPaneGroup::Pane(SerializedPane::new(
+                    self.get_items(pane_id)?,
+                    active,
+                )))
             } else {
                 bail!("Pane Group Child was neither a pane group or a pane");
             }
         })
         // Filter out panes and pane groups which don't have any children or items
-        .filter(|pane_group| {
-            match pane_group {
-                Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
-                Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
-                _ => true,
-            }
+        .filter(|pane_group| match pane_group {
+            Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
+            Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
+            _ => true,
         })
         .collect::<Result<_>>()
     }
 
     pub(crate) fn save_pane_group(
         &self,
-        workspace_id: &WorkspaceId,
+        workspace_id: WorkspaceId,
         pane_group: &SerializedPaneGroup,
         parent: Option<(GroupId, usize)>,
     ) -> Result<()> {
@@ -285,26 +276,31 @@ impl WorkspaceDb {
             SerializedPaneGroup::Group { axis, children } => {
                 let (parent_id, position) = unzip_option(parent);
 
-                let group_id = self.select_row_bound::<_, i64>(indoc!{"
+                let group_id = self.select_row_bound::<_, i64>(indoc! {"
                         INSERT INTO pane_groups(workspace_id, parent_group_id, position, axis) 
                         VALUES (?, ?, ?, ?) 
-                        RETURNING group_id"})?
-                    ((workspace_id, parent_id, position, *axis))?
-                    .ok_or_else(|| anyhow!("Couldn't retrieve group_id from inserted pane_group"))?;
-                
+                        RETURNING group_id"})?((
+                    workspace_id,
+                    parent_id,
+                    position,
+                    *axis,
+                ))?
+                .ok_or_else(|| anyhow!("Couldn't retrieve group_id from inserted pane_group"))?;
+
                 for (position, group) in children.iter().enumerate() {
                     self.save_pane_group(workspace_id, group, Some((group_id, position)))?
                 }
+
                 Ok(())
             }
             SerializedPaneGroup::Pane(pane) => {
                 self.save_pane(workspace_id, &pane, parent, false)?;
                 Ok(())
-            },
+            }
         }
     }
 
-    pub(crate) fn get_dock_pane(&self, workspace_id: &WorkspaceId) -> Result<SerializedPane> {
+    pub(crate) fn get_dock_pane(&self, workspace_id: WorkspaceId) -> Result<SerializedPane> {
         let (pane_id, active) = self.select_row_bound(indoc! {"
             SELECT pane_id, active
             FROM panes
@@ -315,40 +311,35 @@ impl WorkspaceDb {
 
         Ok(SerializedPane::new(
             self.get_items(pane_id).context("Reading items")?,
-            active
+            active,
         ))
     }
 
     pub(crate) fn save_pane(
         &self,
-        workspace_id: &WorkspaceId,
+        workspace_id: WorkspaceId,
         pane: &SerializedPane,
         parent: Option<(GroupId, usize)>, // None indicates BOTH dock pane AND center_pane
         dock: bool,
     ) -> Result<PaneId> {
-        let pane_id = self.select_row_bound::<_, i64>(indoc!{"
+        let pane_id = self.select_row_bound::<_, i64>(indoc! {"
             INSERT INTO panes(workspace_id, active) 
             VALUES (?, ?) 
-            RETURNING pane_id"},
-        )?((workspace_id, pane.active))?
+            RETURNING pane_id"})?((workspace_id, pane.active))?
         .ok_or_else(|| anyhow!("Could not retrieve inserted pane_id"))?;
-        
+
         if !dock {
             let (parent_id, order) = unzip_option(parent);
             self.exec_bound(indoc! {"
                 INSERT INTO center_panes(pane_id, parent_group_id, position)
-                VALUES (?, ?, ?)"})?((
-                    pane_id, parent_id, order
-                ))?;
+                VALUES (?, ?, ?)"})?((pane_id, parent_id, order))?;
         }
 
         self.save_items(workspace_id, pane_id, &pane.children)
             .context("Saving items")?;
-        
+
         Ok(pane_id)
     }
-    
-  
 
     pub(crate) fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
         Ok(self.select_bound(indoc! {"
@@ -359,7 +350,7 @@ impl WorkspaceDb {
 
     pub(crate) fn save_items(
         &self,
-        workspace_id: &WorkspaceId,
+        workspace_id: WorkspaceId,
         pane_id: PaneId,
         items: &[SerializedItem],
     ) -> Result<()> {
@@ -376,7 +367,8 @@ impl WorkspaceDb {
 
 #[cfg(test)]
 mod tests {
-    use db::{open_memory_db};
+
+    use db::{open_memory_db, Uuid};
     use settings::DockAnchor;
 
     use super::*;
@@ -388,15 +380,13 @@ mod tests {
         let db = WorkspaceDb(open_memory_db(Some("test_full_workspace_serialization")));
 
         let dock_pane = crate::persistence::model::SerializedPane {
-            
             children: vec![
                 SerializedItem::new("Terminal", 1),
                 SerializedItem::new("Terminal", 2),
                 SerializedItem::new("Terminal", 3),
                 SerializedItem::new("Terminal", 4),
-
             ],
-            active: false
+            active: false,
         };
 
         //  -----------------
@@ -415,8 +405,8 @@ mod tests {
                                 SerializedItem::new("Terminal", 5),
                                 SerializedItem::new("Terminal", 6),
                             ],
-                            false)
-                        ),
+                            false,
+                        )),
                         SerializedPaneGroup::Pane(SerializedPane::new(
                             vec![
                                 SerializedItem::new("Terminal", 7),
@@ -430,7 +420,6 @@ mod tests {
                     vec![
                         SerializedItem::new("Terminal", 9),
                         SerializedItem::new("Terminal", 10),
-
                     ],
                     false,
                 )),
@@ -438,25 +427,24 @@ mod tests {
         };
 
         let workspace = SerializedWorkspace {
-            workspace_id: (["/tmp", "/tmp2"]).into(),
-            dock_position:  DockPosition::Shown(DockAnchor::Bottom),
+            id: Uuid::new(),
+            location: (["/tmp", "/tmp2"]).into(),
+            dock_position: DockPosition::Shown(DockAnchor::Bottom),
             center_group,
             dock_pane,
         };
-        
-        db.save_workspace(None, &workspace);
+
+        db.save_workspace(&workspace);
         let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
-        
+
         assert_eq!(workspace, round_trip_workspace.unwrap());
 
         // Test guaranteed duplicate IDs
-        db.save_workspace(None, &workspace);
-        db.save_workspace(None, &workspace);
-        
+        db.save_workspace(&workspace);
+        db.save_workspace(&workspace);
+
         let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
         assert_eq!(workspace, round_trip_workspace.unwrap());
-        
-        
     }
 
     #[test]
@@ -466,21 +454,23 @@ mod tests {
         let db = WorkspaceDb(open_memory_db(Some("test_basic_functionality")));
 
         let workspace_1 = SerializedWorkspace {
-            workspace_id: (["/tmp", "/tmp2"]).into(),
+            id: WorkspaceId::new(),
+            location: (["/tmp", "/tmp2"]).into(),
             dock_position: crate::dock::DockPosition::Shown(DockAnchor::Bottom),
             center_group: Default::default(),
             dock_pane: Default::default(),
         };
 
         let mut workspace_2 = SerializedWorkspace {
-            workspace_id: (["/tmp"]).into(),
+            id: WorkspaceId::new(),
+            location: (["/tmp"]).into(),
             dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Expanded),
             center_group: Default::default(),
             dock_pane: Default::default(),
         };
 
-        db.save_workspace(None, &workspace_1);
-        db.save_workspace(None, &workspace_2);
+        db.save_workspace(&workspace_1);
+        db.save_workspace(&workspace_2);
 
         // Test that paths are treated as a set
         assert_eq!(
@@ -497,8 +487,9 @@ mod tests {
         assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
 
         // Test 'mutate' case of updating a pre-existing id
-        workspace_2.workspace_id = (["/tmp", "/tmp2"]).into();
-        db.save_workspace(Some((&["/tmp"]).into()), &workspace_2);
+        workspace_2.location = (["/tmp", "/tmp2"]).into();
+
+        db.save_workspace(&workspace_2);
         assert_eq!(
             db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
             workspace_2
@@ -506,33 +497,28 @@ mod tests {
 
         // Test other mechanism for mutating
         let mut workspace_3 = SerializedWorkspace {
-            workspace_id: (&["/tmp", "/tmp2"]).into(),
+            id: WorkspaceId::new(),
+            location: (&["/tmp", "/tmp2"]).into(),
             dock_position: DockPosition::Shown(DockAnchor::Right),
             center_group: Default::default(),
             dock_pane: Default::default(),
         };
 
-        
-        db.save_workspace(None, &workspace_3);
+        db.save_workspace(&workspace_3);
         assert_eq!(
             db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
             workspace_3
         );
 
         // Make sure that updating paths differently also works
-        workspace_3.workspace_id = (["/tmp3", "/tmp4", "/tmp2"]).into();
-        db.save_workspace(
-            Some((&["/tmp", "/tmp2"]).into()),
-            &workspace_3,
-        );
+        workspace_3.location = (["/tmp3", "/tmp4", "/tmp2"]).into();
+        db.save_workspace(&workspace_3);
         assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
         assert_eq!(
             db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
                 .unwrap(),
             workspace_3
         );
-        
-        
     }
 
     use crate::dock::DockPosition;
@@ -545,7 +531,8 @@ mod tests {
         center_group: &SerializedPaneGroup,
     ) -> SerializedWorkspace {
         SerializedWorkspace {
-            workspace_id: workspace_id.into(),
+            id: WorkspaceId::new(),
+            location: workspace_id.into(),
             dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Right),
             center_group: center_group.clone(),
             dock_pane,
@@ -564,12 +551,13 @@ mod tests {
                 SerializedItem::new("Terminal", 4),
                 SerializedItem::new("Terminal", 2),
                 SerializedItem::new("Terminal", 3),
-            ], false
+            ],
+            false,
         );
 
         let workspace = default_workspace(&["/tmp"], dock_pane, &Default::default());
 
-        db.save_workspace(None, &workspace);
+        db.save_workspace(&workspace);
 
         let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
 
@@ -593,16 +581,20 @@ mod tests {
                 SerializedPaneGroup::Group {
                     axis: gpui::Axis::Vertical,
                     children: vec![
-                    SerializedPaneGroup::Pane(SerializedPane::new( 
-                        vec![
-                            SerializedItem::new("Terminal", 1),
-                            SerializedItem::new("Terminal", 2),
-                        ],
-                        false)),
-                    SerializedPaneGroup::Pane(SerializedPane::new(vec![
-                            SerializedItem::new("Terminal", 4),
-                            SerializedItem::new("Terminal", 3),
-                        ], true)),  
+                        SerializedPaneGroup::Pane(SerializedPane::new(
+                            vec![
+                                SerializedItem::new("Terminal", 1),
+                                SerializedItem::new("Terminal", 2),
+                            ],
+                            false,
+                        )),
+                        SerializedPaneGroup::Pane(SerializedPane::new(
+                            vec![
+                                SerializedItem::new("Terminal", 4),
+                                SerializedItem::new("Terminal", 3),
+                            ],
+                            true,
+                        )),
                     ],
                 },
                 SerializedPaneGroup::Pane(SerializedPane::new(
@@ -610,41 +602,46 @@ mod tests {
                         SerializedItem::new("Terminal", 5),
                         SerializedItem::new("Terminal", 6),
                     ],
-                    false)),
+                    false,
+                )),
             ],
         };
 
         let workspace = default_workspace(&["/tmp"], Default::default(), &center_pane);
 
-        db.save_workspace(None, &workspace);
-                
+        db.save_workspace(&workspace);
+
         let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
 
         assert_eq!(workspace.center_group, new_workspace.center_group);
     }
-    
+
     #[test]
     fn test_cleanup_panes() {
         env_logger::try_init().ok();
-        
+
         let db = WorkspaceDb(open_memory_db(Some("test_cleanup_panes")));
-        
+
         let center_pane = SerializedPaneGroup::Group {
             axis: gpui::Axis::Horizontal,
             children: vec![
                 SerializedPaneGroup::Group {
                     axis: gpui::Axis::Vertical,
                     children: vec![
-                    SerializedPaneGroup::Pane(SerializedPane::new( 
-                        vec![
-                            SerializedItem::new("Terminal", 1),
-                            SerializedItem::new("Terminal", 2),
-                        ],
-                        false)),
-                    SerializedPaneGroup::Pane(SerializedPane::new(vec![
-                            SerializedItem::new("Terminal", 4),
-                            SerializedItem::new("Terminal", 3),
-                        ], true)),  
+                        SerializedPaneGroup::Pane(SerializedPane::new(
+                            vec![
+                                SerializedItem::new("Terminal", 1),
+                                SerializedItem::new("Terminal", 2),
+                            ],
+                            false,
+                        )),
+                        SerializedPaneGroup::Pane(SerializedPane::new(
+                            vec![
+                                SerializedItem::new("Terminal", 4),
+                                SerializedItem::new("Terminal", 3),
+                            ],
+                            true,
+                        )),
                     ],
                 },
                 SerializedPaneGroup::Pane(SerializedPane::new(
@@ -652,37 +649,41 @@ mod tests {
                         SerializedItem::new("Terminal", 5),
                         SerializedItem::new("Terminal", 6),
                     ],
-                    false)),
+                    false,
+                )),
             ],
         };
 
         let id = &["/tmp"];
-        
+
         let mut workspace = default_workspace(id, Default::default(), &center_pane);
 
-        db.save_workspace(None, &workspace);
-        
+        db.save_workspace(&workspace);
+
         workspace.center_group = SerializedPaneGroup::Group {
             axis: gpui::Axis::Vertical,
             children: vec![
-            SerializedPaneGroup::Pane(SerializedPane::new( 
-                vec![
-                    SerializedItem::new("Terminal", 1),
-                    SerializedItem::new("Terminal", 2),
-                ],
-                false)),
-            SerializedPaneGroup::Pane(SerializedPane::new(vec![
-                    SerializedItem::new("Terminal", 4),
-                    SerializedItem::new("Terminal", 3),
-                ], true)),  
+                SerializedPaneGroup::Pane(SerializedPane::new(
+                    vec![
+                        SerializedItem::new("Terminal", 1),
+                        SerializedItem::new("Terminal", 2),
+                    ],
+                    false,
+                )),
+                SerializedPaneGroup::Pane(SerializedPane::new(
+                    vec![
+                        SerializedItem::new("Terminal", 4),
+                        SerializedItem::new("Terminal", 3),
+                    ],
+                    true,
+                )),
             ],
         };
-        
-        db.save_workspace(None, &workspace);
-                
+
+        db.save_workspace(&workspace);
+
         let new_workspace = db.workspace_for_roots(id).unwrap();
 
         assert_eq!(workspace.center_group, new_workspace.center_group);
-
     }
 }

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

@@ -16,18 +16,20 @@ use project::Project;
 use settings::DockAnchor;
 use util::ResultExt;
 
-use crate::{dock::DockPosition, ItemDeserializers, Member, Pane, PaneAxis, Workspace};
+use crate::{
+    dock::DockPosition, ItemDeserializers, Member, Pane, PaneAxis, Workspace, WorkspaceId,
+};
 
 #[derive(Debug, Clone, PartialEq, Eq)]
-pub struct WorkspaceId(Arc<Vec<PathBuf>>);
+pub struct WorkspaceLocation(Arc<Vec<PathBuf>>);
 
-impl WorkspaceId {
+impl WorkspaceLocation {
     pub fn paths(&self) -> Arc<Vec<PathBuf>> {
         self.0.clone()
     }
 }
 
-impl<P: AsRef<Path>, T: IntoIterator<Item = P>> From<T> for WorkspaceId {
+impl<P: AsRef<Path>, T: IntoIterator<Item = P>> From<T> for WorkspaceLocation {
     fn from(iterator: T) -> Self {
         let mut roots = iterator
             .into_iter()
@@ -38,7 +40,7 @@ impl<P: AsRef<Path>, T: IntoIterator<Item = P>> From<T> for WorkspaceId {
     }
 }
 
-impl Bind for &WorkspaceId {
+impl Bind for &WorkspaceLocation {
     fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
         bincode::serialize(&self.0)
             .expect("Bincode serialization of paths should not fail")
@@ -46,16 +48,20 @@ impl Bind for &WorkspaceId {
     }
 }
 
-impl Column for WorkspaceId {
+impl Column for WorkspaceLocation {
     fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
         let blob = statement.column_blob(start_index)?;
-        Ok((WorkspaceId(bincode::deserialize(blob)?), start_index + 1))
+        Ok((
+            WorkspaceLocation(bincode::deserialize(blob)?),
+            start_index + 1,
+        ))
     }
 }
 
 #[derive(Debug, PartialEq, Eq)]
 pub struct SerializedWorkspace {
-    pub workspace_id: WorkspaceId,
+    pub id: WorkspaceId,
+    pub location: WorkspaceLocation,
     pub dock_position: DockPosition,
     pub center_group: SerializedPaneGroup,
     pub dock_pane: SerializedPane,
@@ -70,10 +76,11 @@ pub enum SerializedPaneGroup {
     Pane(SerializedPane),
 }
 
+#[cfg(test)]
 impl Default for SerializedPaneGroup {
     fn default() -> Self {
         Self::Pane(SerializedPane {
-            children: Vec::new(),
+            children: vec![SerializedItem::default()],
             active: false,
         })
     }
@@ -84,7 +91,7 @@ impl SerializedPaneGroup {
     pub(crate) async fn deserialize(
         &self,
         project: &ModelHandle<Project>,
-        workspace_id: &WorkspaceId,
+        workspace_id: WorkspaceId,
         workspace: &ViewHandle<Workspace>,
         cx: &mut AsyncAppContext,
     ) -> (Member, Option<ViewHandle<Pane>>) {
@@ -136,13 +143,12 @@ impl SerializedPane {
         &self,
         project: &ModelHandle<Project>,
         pane_handle: &ViewHandle<Pane>,
-        workspace_id: &WorkspaceId,
+        workspace_id: WorkspaceId,
         workspace: &ViewHandle<Workspace>,
         cx: &mut AsyncAppContext,
     ) {
         for item in self.children.iter() {
             let project = project.clone();
-            let workspace_id = workspace_id.clone();
             let item_handle = pane_handle
                 .update(cx, |_, cx| {
                     if let Some(deserializer) = cx.global::<ItemDeserializers>().get(&item.kind) {
@@ -191,6 +197,16 @@ impl SerializedItem {
     }
 }
 
+#[cfg(test)]
+impl Default for SerializedItem {
+    fn default() -> Self {
+        SerializedItem {
+            kind: Arc::from("Terminal"),
+            item_id: 100000,
+        }
+    }
+}
+
 impl Bind for &SerializedItem {
     fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
         let next_index = statement.bind(self.kind.clone(), start_index)?;
@@ -231,7 +247,7 @@ mod tests {
     use db::sqlez::connection::Connection;
     use settings::DockAnchor;
 
-    use super::WorkspaceId;
+    use super::WorkspaceLocation;
 
     #[test]
     fn test_workspace_round_trips() {
@@ -245,7 +261,7 @@ mod tests {
             .unwrap()()
         .unwrap();
 
-        let workspace_id: WorkspaceId = WorkspaceId::from(&["\test2", "\test1"]);
+        let workspace_id: WorkspaceLocation = WorkspaceLocation::from(&["\test2", "\test1"]);
 
         db.exec_bound("INSERT INTO workspace_id_test(workspace_id, dock_anchor) VALUES (?,?)")
             .unwrap()((&workspace_id, DockAnchor::Bottom))
@@ -255,7 +271,10 @@ mod tests {
             db.select_row("SELECT workspace_id, dock_anchor FROM workspace_id_test LIMIT 1")
                 .unwrap()()
             .unwrap(),
-            Some((WorkspaceId::from(&["\test1", "\test2"]), DockAnchor::Bottom))
+            Some((
+                WorkspaceLocation::from(&["\test1", "\test2"]),
+                DockAnchor::Bottom
+            ))
         );
     }
 }

crates/workspace/src/shared_screen.rs 🔗

@@ -1,7 +1,5 @@
 use crate::{
-    item::ItemEvent,
-    persistence::model::{ItemId, WorkspaceId},
-    Item, ItemNavHistory, Pane, Workspace,
+    item::ItemEvent, persistence::model::ItemId, Item, ItemNavHistory, Pane, Workspace, WorkspaceId,
 };
 use anyhow::{anyhow, Result};
 use call::participant::{Frame, RemoteVideoTrack};
@@ -148,7 +146,11 @@ impl Item for SharedScreen {
         self.nav_history = Some(history);
     }
 
-    fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self> {
+    fn clone_on_split(
+        &self,
+        _workspace_id: WorkspaceId,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Self> {
         let track = self.track.upgrade()?;
         Some(Self::new(&track, self.peer_id, self.user.clone(), cx))
     }

crates/workspace/src/workspace.rs 🔗

@@ -26,6 +26,7 @@ use anyhow::{anyhow, Context, Result};
 use call::ActiveCall;
 use client::{proto, Client, PeerId, TypedEnvelope, UserStore};
 use collections::{hash_map, HashMap, HashSet};
+use db::Uuid;
 use dock::{DefaultItemFactory, Dock, ToggleDockButton};
 use drag_and_drop::DragAndDrop;
 use fs::{self, Fs};
@@ -45,7 +46,7 @@ use log::{error, warn};
 pub use pane::*;
 pub use pane_group::*;
 use persistence::model::SerializedItem;
-pub use persistence::model::{ItemId, WorkspaceId};
+pub use persistence::model::{ItemId, WorkspaceLocation};
 use postage::prelude::Stream;
 use project::{Project, ProjectEntryId, ProjectPath, ProjectStore, Worktree, WorktreeId};
 use serde::Deserialize;
@@ -128,6 +129,8 @@ pub struct OpenProjectEntryInPane {
     project_entry: ProjectEntryId,
 }
 
+pub type WorkspaceId = Uuid;
+
 impl_internal_actions!(
     workspace,
     [
@@ -530,6 +533,7 @@ pub struct Workspace {
     last_leaders_by_pane: HashMap<WeakViewHandle<Pane>, PeerId>,
     window_edited: bool,
     active_call: Option<(ModelHandle<ActiveCall>, Vec<gpui::Subscription>)>,
+    database_id: WorkspaceId,
     _observe_current_user: Task<()>,
 }
 
@@ -556,7 +560,7 @@ impl Workspace {
                 project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded => {
                     this.update_window_title(cx);
                     // TODO: Cache workspace_id on workspace and read from it here
-                    this.serialize_workspace(None, cx);
+                    this.serialize_workspace(cx);
                 }
                 project::Event::DisconnectedFromHost => {
                     this.update_window_edited(cx);
@@ -630,6 +634,12 @@ impl Workspace {
             active_call = Some((call, subscriptions));
         }
 
+        let id = if let Some(id) = serialized_workspace.as_ref().map(|ws| ws.id) {
+            id
+        } else {
+            WorkspaceId::new()
+        };
+
         let mut this = Workspace {
             modal: None,
             weak_self: weak_handle.clone(),
@@ -657,6 +667,7 @@ impl Workspace {
             last_leaders_by_pane: Default::default(),
             window_edited: false,
             active_call,
+            database_id: id,
             _observe_current_user,
         };
         this.project_remote_id_changed(project.read(cx).remote_id(), cx);
@@ -1317,7 +1328,7 @@ impl Workspace {
     pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
         let active_pane = self.active_pane().clone();
         Pane::add_item(self, &active_pane, item, true, true, None, cx);
-        self.serialize_workspace(None, cx);
+        self.serialize_workspace(cx);
     }
 
     pub fn open_path(
@@ -1522,7 +1533,7 @@ impl Workspace {
                             entry.remove();
                         }
                     }
-                    self.serialize_workspace(None, cx);
+                    self.serialize_workspace(cx);
                 }
                 _ => {}
             }
@@ -1544,7 +1555,7 @@ impl Workspace {
 
         pane.read(cx).active_item().map(|item| {
             let new_pane = self.add_pane(cx);
-            if let Some(clone) = item.clone_on_split(cx.as_mut()) {
+            if let Some(clone) = item.clone_on_split(self.database_id(), cx.as_mut()) {
                 Pane::add_item(self, &new_pane, clone, true, true, None, cx);
             }
             self.center.split(&pane, &new_pane, direction).unwrap();
@@ -2255,7 +2266,11 @@ impl Workspace {
         }
     }
 
-    fn workspace_id(&self, cx: &AppContext) -> WorkspaceId {
+    pub fn database_id(&self) -> WorkspaceId {
+        self.database_id
+    }
+
+    fn location(&self, cx: &AppContext) -> WorkspaceLocation {
         self.project()
             .read(cx)
             .visible_worktrees(cx)
@@ -2275,7 +2290,7 @@ impl Workspace {
         }
     }
 
-    fn serialize_workspace(&self, old_id: Option<WorkspaceId>, cx: &AppContext) {
+    fn serialize_workspace(&self, cx: &AppContext) {
         fn serialize_pane_handle(
             pane_handle: &ViewHandle<Pane>,
             cx: &AppContext,
@@ -2320,7 +2335,8 @@ impl Workspace {
         let center_group = build_serialized_pane_group(&self.center.root, cx);
 
         let serialized_workspace = SerializedWorkspace {
-            workspace_id: self.workspace_id(cx),
+            id: self.database_id,
+            location: self.location(cx),
             dock_position: self.dock.position(),
             dock_pane,
             center_group,
@@ -2328,7 +2344,7 @@ impl Workspace {
 
         cx.background()
             .spawn(async move {
-                persistence::DB.save_workspace(old_id, &serialized_workspace);
+                persistence::DB.save_workspace(&serialized_workspace);
             })
             .detach();
     }
@@ -2349,7 +2365,7 @@ impl Workspace {
                     .deserialize_to(
                         &project,
                         &dock_pane_handle,
-                        &serialized_workspace.workspace_id,
+                        serialized_workspace.id,
                         &workspace,
                         &mut cx,
                     )
@@ -2359,12 +2375,7 @@ impl Workspace {
 
                 let (root, active_pane) = serialized_workspace
                     .center_group
-                    .deserialize(
-                        &project,
-                        &serialized_workspace.workspace_id,
-                        &workspace,
-                        &mut cx,
-                    )
+                    .deserialize(&project, serialized_workspace.id, &workspace, &mut cx)
                     .await;
 
                 // Remove old panes from workspace panes list

crates/zed/src/main.rs 🔗

@@ -597,6 +597,8 @@ pub fn default_item_factory(
 
     let working_directory = get_working_directory(workspace, cx, strategy);
 
-    let terminal_handle = cx.add_view(|cx| TerminalContainer::new(working_directory, false, cx));
+    let terminal_handle = cx.add_view(|cx| {
+        TerminalContainer::new(working_directory, false, workspace.database_id(), cx)
+    });
     Box::new(terminal_handle)
 }