diff --git a/crates/auto_update/src/update_notification.rs b/crates/auto_update/src/update_notification.rs index 9963ae65b8..5fbdf17422 100644 --- a/crates/auto_update/src/update_notification.rs +++ b/crates/auto_update/src/update_notification.rs @@ -30,7 +30,7 @@ impl View for UpdateNotification { let theme = cx.global::().theme.clone(); let theme = &theme.update_notification; - let app_name = cx.global::().name(); + let app_name = cx.global::().display_name(); MouseEventHandler::::new(0, cx, |state, cx| { Flex::column() diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index a81f33c604..ce8b713996 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -106,7 +106,7 @@ impl Telemetry { pub fn new(client: Arc, cx: &AppContext) -> Arc { let platform = cx.platform(); let release_channel = if cx.has_global::() { - Some(cx.global::().name()) + Some(cx.global::().display_name()) } else { None }; diff --git a/crates/db/src/db.rs b/crates/db/src/db.rs index 7b214cb3be..c146336132 100644 --- a/crates/db/src/db.rs +++ b/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 = 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(wipe_db: bool, db_dir: &Path, release_channel: &ReleaseChannel) -> ThreadSafeConnection { - 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(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(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(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(); + impl Domain for TestDB { + fn name() -> &'static str { + "db_tests" + } - let test_db = open_db::(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::(true, &path, &ReleaseChannel::Dev).await; - - assert!(test_db.select_row::<()>(sql!(SELECT value FROM test)).unwrap()().unwrap().is_none()) - })); + fn migrations() -> &'static [&'static str] { + &[sql!( + CREATE TABLE test(value); + )] + } + } + + let tempdir = TempDir::new("DbTests").unwrap(); + + // Create a db and insert a marker value + let test_db = open_db::(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::(true, &path, &ReleaseChannel::Dev).await; - guards.push(guard); + 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 + #[gpui::test] + #[should_panic] + async fn test_bad_migration_panics() { + enum BadDB {} + + impl Domain for BadDB { + fn name() -> &'static str { + "db_tests" } - for guard in guards { - guard.join().unwrap(); + fn migrations() -> &'static [&'static str] { + &[sql!(CREATE TABLE test(value);), + // failure because test already exists + 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() { - - } - - // Test bad migration panics - #[test] - fn test_bad_migration_panics() { - + } + + let tempdir = TempDir::new("DbTests").unwrap(); + let _bad_db = open_db::(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);)] + } + } - // open_db(db_dir, release_channel) + 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::(false, tempdir.path(), &util::channel::ReleaseChannel::Dev).await; + assert!(corrupt_db.persistent()); + } + + let good_db = open_db::(false, tempdir.path(), &util::channel::ReleaseChannel::Dev).await; + assert!(good_db.select_row::("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); + + let backup = Connection::open_file(&corrupted_backup_dir.to_string_lossy()); + assert!(backup.select_row::("SELECT * FROM test").unwrap()().unwrap().is_none()); } } diff --git a/crates/sqlez/src/migrations.rs b/crates/sqlez/src/migrations.rs index aa8d5fe00b..41c505f85b 100644 --- a/crates/sqlez/src/migrations.rs +++ b/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))?; } diff --git a/crates/sqlez/src/thread_safe_connection.rs b/crates/sqlez/src/thread_safe_connection.rs index 7b89827979..51d0707fd8 100644 --- a/crates/sqlez/src/thread_safe_connection.rs +++ b/crates/sqlez/src/thread_safe_connection.rs @@ -96,7 +96,6 @@ impl ThreadSafeConnectionBuilder { .with_savepoint("thread_safe_multi_migration", || M::migrate(connection)); if migration_result.is_ok() { - println!("Migration succeded"); break; } } diff --git a/crates/util/src/channel.rs b/crates/util/src/channel.rs index ab5b53b4ab..3edf26dc95 100644 --- a/crates/util/src/channel.rs +++ b/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", + } + } } diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 917f821e4a..5894a2a44e 100644 --- a/crates/workspace/Cargo.toml +++ b/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"] } diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 0879166bbe..9b1342ecd9 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -175,16 +175,21 @@ impl Dock { new_position: DockPosition, cx: &mut ViewContext, ) { + 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(); } diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index a0cc48ca1c..2d4ae919f9 100644 --- a/crates/workspace/src/persistence.rs +++ b/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> { 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 } } diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index c57c992d7b..c75488561f 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/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, cx: &mut AsyncAppContext, - ) -> (Member, Option>) { + ) -> Option<(Member, Option>)> { 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 + } } } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 8e9131839d..5fb804e66d 100644 --- a/crates/workspace/src/workspace.rs +++ b/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::>() - .into() + fn location(&self, cx: &AppContext) -> Option { + let project = self.project().read(cx); + + if project.is_local() { + Some( + project + .visible_worktrees(cx) + .map(|worktree| worktree.read(cx).abs_path()) + .collect::>() + .into(), + ) + } else { + None + } } fn remove_panes(&mut self, member: Member, cx: &mut ViewContext) { @@ -2331,24 +2346,24 @@ impl Workspace { } } - let location = self.location(cx); + 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); - 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(), + }; - 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(); + 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(root); + // 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(); + + 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(); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 0a25cfb66f..d86e449ff2 100644 --- a/crates/zed/src/zed.rs +++ b/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) { - let app_name = cx.global::().name(); + let app_name = cx.global::().display_name(); let version = env!("CARGO_PKG_VERSION"); cx.prompt( gpui::PromptLevel::Info,