diff --git a/Cargo.lock b/Cargo.lock index 9e3da4e673..37a14513cb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -616,6 +616,17 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "cache-padded" version = "1.1.1" @@ -2506,6 +2517,21 @@ dependencies = [ "libc", ] +[[package]] +name = "librocksdb-sys" +version = "0.6.1+6.28.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81bc587013734dadb7cf23468e531aa120788b87243648be42e2d3a072186291" +dependencies = [ + "bindgen", + "bzip2-sys", + "cc", + "glob", + "libc", + "libz-sys", + "zstd-sys", +] + [[package]] name = "libz-sys" version = "1.1.3" @@ -3395,6 +3421,7 @@ dependencies = [ "postage", "rand 0.8.3", "regex", + "rocksdb", "rpc", "serde", "serde_json", @@ -3713,9 +3740,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.5.4" +version = "1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" +checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" dependencies = [ "aho-corasick", "memchr", @@ -3733,9 +3760,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.25" +version = "0.6.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" +checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" [[package]] name = "remove_dir_all" @@ -3822,6 +3849,16 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "rocksdb" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "620f4129485ff1a7128d184bc687470c21c7951b64779ebc9cfdad3dcd920290" +dependencies = [ + "libc", + "librocksdb-sys", +] + [[package]] name = "roxmltree" version = "0.14.1" diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index 96ed7714c2..a9e319681f 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -4604,7 +4604,7 @@ impl TestServer { }); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx)); - let project_store = cx.add_model(|_| ProjectStore::default()); + let project_store = cx.add_model(|_| ProjectStore::new(project::Db::open_fake())); let app_state = Arc::new(workspace::AppState { client: client.clone(), user_store: user_store.clone(), diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index 9be9906f8f..e2a7971704 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -1175,7 +1175,7 @@ mod tests { let http_client = FakeHttpClient::with_404_response(); let client = Client::new(http_client.clone()); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); - let project_store = cx.add_model(|_| ProjectStore::default()); + let project_store = cx.add_model(|_| ProjectStore::new(project::Db::open_fake())); let server = FakeServer::for_client(current_user_id, &client, &cx).await; let fs = FakeFs::new(cx.background()); fs.insert_tree("/private_dir", json!({ "one.rs": "" })) diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index 921eb9ddc5..1fb989ee9c 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -47,6 +47,7 @@ similar = "1.3" smol = "1.2.5" thiserror = "1.0.29" toml = "0.5" +rocksdb = "0.18" [dev-dependencies] client = { path = "../client", features = ["test-support"] } diff --git a/crates/project/src/db.rs b/crates/project/src/db.rs new file mode 100644 index 0000000000..bc125e4303 --- /dev/null +++ b/crates/project/src/db.rs @@ -0,0 +1,161 @@ +use anyhow::Result; +use std::path::PathBuf; +use std::sync::Arc; + +pub struct Db(DbStore); + +enum DbStore { + Null, + Real(rocksdb::DB), + + #[cfg(any(test, feature = "test-support"))] + Fake { + data: parking_lot::Mutex, Vec>>, + }, +} + +impl Db { + /// Open or create a database at the given file path. + pub fn open(path: PathBuf) -> Result> { + let db = rocksdb::DB::open_default(&path)?; + Ok(Arc::new(Self(DbStore::Real(db)))) + } + + /// Open a null database that stores no data, for use as a fallback + /// when there is an error opening the real database. + pub fn null() -> Arc { + Arc::new(Self(DbStore::Null)) + } + + /// Open a fake database for testing. + #[cfg(any(test, feature = "test-support"))] + pub fn open_fake() -> Arc { + Arc::new(Self(DbStore::Fake { + data: Default::default(), + })) + } + + pub fn read(&self, keys: I) -> Result>>> + where + K: AsRef<[u8]>, + I: IntoIterator, + { + match &self.0 { + DbStore::Real(db) => db + .multi_get(keys) + .into_iter() + .map(|e| e.map_err(Into::into)) + .collect(), + + DbStore::Null => Ok(keys.into_iter().map(|_| None).collect()), + + #[cfg(any(test, feature = "test-support"))] + DbStore::Fake { data: db } => { + let db = db.lock(); + Ok(keys + .into_iter() + .map(|key| db.get(key.as_ref()).cloned()) + .collect()) + } + } + } + + pub fn delete(&self, keys: I) -> Result<()> + where + K: AsRef<[u8]>, + I: IntoIterator, + { + match &self.0 { + DbStore::Real(db) => { + let mut batch = rocksdb::WriteBatch::default(); + for key in keys { + batch.delete(key); + } + db.write(batch)?; + } + + DbStore::Null => {} + + #[cfg(any(test, feature = "test-support"))] + DbStore::Fake { data: db } => { + let mut db = db.lock(); + for key in keys { + db.remove(key.as_ref()); + } + } + } + Ok(()) + } + + pub fn write(&self, entries: I) -> Result<()> + where + K: AsRef<[u8]>, + V: AsRef<[u8]>, + I: IntoIterator, + { + match &self.0 { + DbStore::Real(db) => { + let mut batch = rocksdb::WriteBatch::default(); + for (key, value) in entries { + batch.put(key, value); + } + db.write(batch)?; + } + + DbStore::Null => {} + + #[cfg(any(test, feature = "test-support"))] + DbStore::Fake { data: db } => { + let mut db = db.lock(); + for (key, value) in entries { + db.insert(key.as_ref().into(), value.as_ref().into()); + } + } + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tempdir::TempDir; + + #[gpui::test] + fn test_db() { + let dir = TempDir::new("db-test").unwrap(); + let fake_db = Db::open_fake(); + let real_db = Db::open(dir.path().join("test.db")).unwrap(); + + for db in [&real_db, &fake_db] { + assert_eq!( + db.read(["key-1", "key-2", "key-3"]).unwrap(), + &[None, None, None] + ); + + db.write([("key-1", "one"), ("key-3", "three")]).unwrap(); + assert_eq!( + db.read(["key-1", "key-2", "key-3"]).unwrap(), + &[ + Some("one".as_bytes().to_vec()), + None, + Some("three".as_bytes().to_vec()) + ] + ); + + db.delete(["key-3", "key-4"]).unwrap(); + assert_eq!( + db.read(["key-1", "key-2", "key-3"]).unwrap(), + &[Some("one".as_bytes().to_vec()), None, None,] + ); + } + + drop(real_db); + + let real_db = Db::open(dir.path().join("test.db")).unwrap(); + assert_eq!( + real_db.read(["key-1", "key-2", "key-3"]).unwrap(), + &[Some("one".as_bytes().to_vec()), None, None,] + ); + } +} diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index a9f7b59a31..a000b32492 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1,3 +1,4 @@ +mod db; pub mod fs; mod ignore; mod lsp_command; @@ -53,6 +54,7 @@ use std::{ use thiserror::Error; use util::{post_inc, ResultExt, TryFutureExt as _}; +pub use db::Db; pub use fs::*; pub use worktree::*; @@ -60,8 +62,8 @@ pub trait Item: Entity { fn entry_id(&self, cx: &AppContext) -> Option; } -#[derive(Default)] pub struct ProjectStore { + db: Arc, projects: Vec>, } @@ -533,7 +535,7 @@ impl Project { let http_client = client::test::FakeHttpClient::with_404_response(); let client = client::Client::new(http_client.clone()); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); - let project_store = cx.add_model(|_| ProjectStore::default()); + let project_store = cx.add_model(|_| ProjectStore::new(Db::open_fake())); let project = cx.update(|cx| { Project::local(true, client, user_store, project_store, languages, fs, cx) }); @@ -568,6 +570,10 @@ impl Project { self.user_store.clone() } + pub fn project_store(&self) -> ModelHandle { + self.project_store.clone() + } + #[cfg(any(test, feature = "test-support"))] pub fn check_invariants(&self, cx: &AppContext) { if self.is_local() { @@ -743,9 +749,6 @@ impl Project { } fn metadata_changed(&mut self, cx: &mut ModelContext) { - cx.notify(); - self.project_store.update(cx, |_, cx| cx.notify()); - if let ProjectClientState::Local { remote_id_rx, public_rx, @@ -768,6 +771,9 @@ impl Project { }) .log_err(); } + + self.project_store.update(cx, |_, cx| cx.notify()); + cx.notify(); } } @@ -5215,6 +5221,13 @@ impl Project { } impl ProjectStore { + pub fn new(db: Arc) -> Self { + Self { + db, + projects: Default::default(), + } + } + pub fn projects<'a>( &'a self, cx: &'a AppContext, @@ -5248,6 +5261,56 @@ impl ProjectStore { cx.notify(); } } + + pub fn are_all_project_paths_public( + &self, + project: &Project, + cx: &AppContext, + ) -> Task> { + let project_path_keys = self.project_path_keys(project, cx); + let db = self.db.clone(); + cx.background().spawn(async move { + let values = db.read(project_path_keys)?; + Ok(values.into_iter().all(|e| e.is_some())) + }) + } + + pub fn set_project_paths_public( + &self, + project: &Project, + public: bool, + cx: &AppContext, + ) -> Task> { + let project_path_keys = self.project_path_keys(project, cx); + let db = self.db.clone(); + cx.background().spawn(async move { + if public { + db.write(project_path_keys.into_iter().map(|key| (key, &[]))) + } else { + db.delete(project_path_keys) + } + }) + } + + fn project_path_keys(&self, project: &Project, cx: &AppContext) -> Vec { + project + .worktrees + .iter() + .filter_map(|worktree| { + worktree.upgrade(&cx).map(|worktree| { + format!( + "public-project-path:{}", + worktree + .read(cx) + .as_local() + .unwrap() + .abs_path() + .to_string_lossy() + ) + }) + }) + .collect::>() + } } impl WorktreeHandle { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index f6b8c5db09..6cb3d36e10 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -692,7 +692,7 @@ impl AppState { let languages = Arc::new(LanguageRegistry::test()); let http_client = client::test::FakeHttpClient::with_404_response(); let client = Client::new(http_client.clone()); - let project_store = cx.add_model(|_| ProjectStore::default()); + let project_store = cx.add_model(|_| ProjectStore::new(project::Db::open_fake())); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx)); let themes = ThemeRegistry::new((), cx.font_cache().clone()); Arc::new(Self { @@ -1055,8 +1055,15 @@ impl Workspace { .clone() .unwrap_or_else(|| self.project.clone()); project.update(cx, |project, cx| { - let is_public = project.is_public(); - project.set_public(!is_public, cx); + let public = !project.is_public(); + eprintln!("toggle_project_public => {}", public); + project.set_public(public, cx); + project.project_store().update(cx, |store, cx| { + store + .set_project_paths_public(project, public, cx) + .detach_and_log_err(cx); + cx.notify(); + }); }); } @@ -2407,6 +2414,7 @@ pub fn open_paths( let app_state = app_state.clone(); let abs_paths = abs_paths.to_vec(); cx.spawn(|mut cx| async move { + let mut new_project = None; let workspace = if let Some(existing) = existing { existing } else { @@ -2416,18 +2424,17 @@ pub fn open_paths( .contains(&false); cx.add_window((app_state.build_window_options)(), |cx| { - let mut workspace = Workspace::new( - Project::local( - false, - app_state.client.clone(), - app_state.user_store.clone(), - app_state.project_store.clone(), - app_state.languages.clone(), - app_state.fs.clone(), - cx, - ), + let project = Project::local( + false, + app_state.client.clone(), + app_state.user_store.clone(), + app_state.project_store.clone(), + app_state.languages.clone(), + app_state.fs.clone(), cx, ); + new_project = Some(project.clone()); + let mut workspace = Workspace::new(project, cx); (app_state.initialize_workspace)(&mut workspace, &app_state, cx); if contains_directory { workspace.toggle_sidebar_item( @@ -2446,6 +2453,26 @@ pub fn open_paths( let items = workspace .update(&mut cx, |workspace, cx| workspace.open_paths(abs_paths, cx)) .await; + + if let Some(project) = new_project { + let public = project + .read_with(&cx, |project, cx| { + app_state + .project_store + .read(cx) + .are_all_project_paths_public(project, cx) + }) + .await + .log_err() + .unwrap_or(false); + if public { + project.update(&mut cx, |project, cx| { + eprintln!("initialize new project public"); + project.set_public(true, cx); + }); + } + } + (workspace, items) }) } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 1427343e4b..dbe797c9fb 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -48,9 +48,10 @@ use zed::{ fn main() { let http = http::client(); - let logs_dir_path = dirs::home_dir() - .expect("could not find home dir") - .join("Library/Logs/Zed"); + let home_dir = dirs::home_dir().expect("could not find home dir"); + let db_dir_path = home_dir.join("Library/Application Support/Zed"); + let logs_dir_path = home_dir.join("Library/Logs/Zed"); + fs::create_dir_all(&db_dir_path).expect("could not create database path"); fs::create_dir_all(&logs_dir_path).expect("could not create logs path"); init_logger(&logs_dir_path); @@ -59,6 +60,11 @@ fn main() { .or_else(|| app.platform().app_version().ok()) .map_or("dev".to_string(), |v| v.to_string()); init_panic_hook(logs_dir_path, app_version, http.clone(), app.background()); + let db = app.background().spawn(async move { + project::Db::open(db_dir_path.join("zed.db")) + .log_err() + .unwrap_or(project::Db::null()) + }); load_embedded_fonts(&app); @@ -136,7 +142,6 @@ fn main() { let client = client::Client::new(http.clone()); let mut languages = languages::build_language_registry(login_shell_env_loaded); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx)); - let project_store = cx.add_model(|_| ProjectStore::default()); context_menu::init(cx); auto_update::init(http, client::ZED_SERVER_URL.clone(), cx); @@ -156,6 +161,7 @@ fn main() { search::init(cx); vim::init(cx); + let db = cx.background().block(db); let (settings_file, keymap_file) = cx.background().block(config_files).unwrap(); let mut settings_rx = settings_from_files( default_settings, @@ -191,6 +197,7 @@ fn main() { .detach(); cx.set_global(settings); + let project_store = cx.add_model(|_| ProjectStore::new(db)); let app_state = Arc::new(AppState { languages, themes,