diff --git a/Cargo.lock b/Cargo.lock index 74860439dd..bad036a05d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1040,7 +1040,6 @@ dependencies = [ "client", "collections", "ctor", - "db", "editor", "env_logger", "envy", @@ -2428,6 +2427,7 @@ dependencies = [ "simplelog", "smallvec", "smol", + "sqlez", "sum_tree", "time 0.3.17", "tiny-skia", @@ -5307,6 +5307,7 @@ dependencies = [ "serde", "serde_json", "serde_path_to_error", + "sqlez", "theme", "toml", "tree-sitter", @@ -7633,6 +7634,7 @@ dependencies = [ "gpui", "indoc", "language", + "lazy_static", "log", "menu", "parking_lot 0.11.2", diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index d6eaaab826..2a8d2fcf05 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -2,17 +2,17 @@ mod update_notification; use anyhow::{anyhow, Context, Result}; use client::{http::HttpClient, ZED_SECRET_CLIENT_TOKEN}; -use db::{kvp::KeyValue, Db}; +use db::kvp::KEY_VALUE_STORE; use gpui::{ actions, platform::AppVersion, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, WeakViewHandle, }; use lazy_static::lazy_static; use serde::Deserialize; -use settings::ReleaseChannel; use smol::{fs::File, io::AsyncReadExt, process::Command}; use std::{env, ffi::OsString, path::PathBuf, sync::Arc, time::Duration}; use update_notification::UpdateNotification; +use util::channel::ReleaseChannel; use workspace::Workspace; const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification"; @@ -42,7 +42,6 @@ pub struct AutoUpdater { current_version: AppVersion, http_client: Arc, pending_poll: Option>, - db: project::Db, server_url: String, } @@ -56,16 +55,11 @@ impl Entity for AutoUpdater { type Event = (); } -pub fn init( - db: Db, - http_client: Arc, - server_url: String, - cx: &mut MutableAppContext, -) { +pub fn init(http_client: Arc, server_url: String, cx: &mut MutableAppContext) { if let Some(version) = (*ZED_APP_VERSION).or_else(|| cx.platform().app_version().ok()) { let server_url = server_url; let auto_updater = cx.add_model(|cx| { - let updater = AutoUpdater::new(version, db, http_client, server_url.clone()); + let updater = AutoUpdater::new(version, http_client, server_url.clone()); updater.start_polling(cx).detach(); updater }); @@ -126,14 +120,12 @@ impl AutoUpdater { fn new( current_version: AppVersion, - db: project::Db, http_client: Arc, server_url: String, ) -> Self { Self { status: AutoUpdateStatus::Idle, current_version, - db, http_client, server_url, pending_poll: None, @@ -303,20 +295,21 @@ impl AutoUpdater { should_show: bool, cx: &AppContext, ) -> Task> { - let db = self.db.clone(); cx.background().spawn(async move { if should_show { - db.write_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY, "")?; + KEY_VALUE_STORE.write_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY, "")?; } else { - db.delete_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?; + KEY_VALUE_STORE.delete_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?; } Ok(()) }) } fn should_show_update_notification(&self, cx: &AppContext) -> Task> { - let db = self.db.clone(); - cx.background() - .spawn(async move { Ok(db.read_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?.is_some()) }) + cx.background().spawn(async move { + Ok(KEY_VALUE_STORE + .read_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)? + .is_some()) + }) } } diff --git a/crates/auto_update/src/update_notification.rs b/crates/auto_update/src/update_notification.rs index 133a197f15..9963ae65b8 100644 --- a/crates/auto_update/src/update_notification.rs +++ b/crates/auto_update/src/update_notification.rs @@ -5,7 +5,8 @@ use gpui::{ Element, Entity, MouseButton, View, ViewContext, }; use menu::Cancel; -use settings::{ReleaseChannel, Settings}; +use settings::Settings; +use util::channel::ReleaseChannel; use workspace::Notification; pub struct UpdateNotification { diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 907f7e80f1..f9b3a88545 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -11,7 +11,6 @@ use async_tungstenite::tungstenite::{ error::Error as WebsocketError, http::{Request, StatusCode}, }; -use db::{kvp::KeyValue, Db}; use futures::{future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, StreamExt, TryStreamExt}; use gpui::{ actions, @@ -27,7 +26,6 @@ use postage::watch; use rand::prelude::*; use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, RequestMessage}; use serde::Deserialize; -use settings::ReleaseChannel; use std::{ any::TypeId, collections::HashMap, @@ -41,6 +39,7 @@ use std::{ use telemetry::Telemetry; use thiserror::Error; use url::Url; +use util::channel::ReleaseChannel; use util::{ResultExt, TryFutureExt}; pub use rpc::*; @@ -1218,8 +1217,8 @@ impl Client { self.peer.respond_with_error(receipt, error) } - pub fn start_telemetry(&self, db: Db) { - self.telemetry.start(db.clone()); + pub fn start_telemetry(&self) { + self.telemetry.start(); } pub fn report_event(&self, kind: &str, properties: Value) { diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 16a7c1cc82..0ce1a07f1b 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -1,5 +1,5 @@ use crate::http::HttpClient; -use db::{kvp::KeyValue, Db}; +use db::kvp::KEY_VALUE_STORE; use gpui::{ executor::Background, serde_json::{self, value::Map, Value}, @@ -10,7 +10,6 @@ use lazy_static::lazy_static; use parking_lot::Mutex; use serde::Serialize; use serde_json::json; -use settings::ReleaseChannel; use std::{ io::Write, mem, @@ -19,7 +18,7 @@ use std::{ time::{Duration, SystemTime, UNIX_EPOCH}, }; use tempfile::NamedTempFile; -use util::{post_inc, ResultExt, TryFutureExt}; +use util::{channel::ReleaseChannel, post_inc, ResultExt, TryFutureExt}; use uuid::Uuid; pub struct Telemetry { @@ -148,18 +147,19 @@ impl Telemetry { Some(self.state.lock().log_file.as_ref()?.path().to_path_buf()) } - pub fn start(self: &Arc, db: Db) { + pub fn start(self: &Arc) { let this = self.clone(); self.executor .spawn( async move { - let device_id = if let Ok(Some(device_id)) = db.read_kvp("device_id") { - device_id - } else { - let device_id = Uuid::new_v4().to_string(); - db.write_kvp("device_id", &device_id)?; - device_id - }; + let device_id = + if let Ok(Some(device_id)) = KEY_VALUE_STORE.read_kvp("device_id") { + device_id + } else { + let device_id = Uuid::new_v4().to_string(); + KEY_VALUE_STORE.write_kvp("device_id", &device_id)?; + device_id + }; let device_id: Arc = device_id.into(); let mut state = this.state.lock(); diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 1722d3374a..09f379526e 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -18,7 +18,6 @@ live_kit_server = { path = "../live_kit_server" } rpc = { path = "../rpc" } util = { path = "../util" } -db = { path = "../db" } anyhow = "1.0.40" async-trait = "0.1.50" async-tungstenite = "0.16" diff --git a/crates/collab/src/integration_tests.rs b/crates/collab/src/integration_tests.rs index bfc14618ea..ade4e10280 100644 --- a/crates/collab/src/integration_tests.rs +++ b/crates/collab/src/integration_tests.rs @@ -1,6 +1,6 @@ use crate::{ - db::{Db, NewUserParams, ProjectId, UserId}, - rpc::{Executor, Server}, + db::{NewUserParams, ProjectId, TestDb, UserId}, + rpc::{Executor, Server, Store}, AppState, }; @@ -12,7 +12,6 @@ use client::{ User, UserStore, RECEIVE_TIMEOUT, }; use collections::{BTreeMap, HashMap, HashSet}; -use db as SqliteDb; use editor::{ self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Redo, Rename, ToOffset, ToggleCodeActions, Undo, @@ -5838,11 +5837,7 @@ impl TestServer { Project::init(&client); cx.update(|cx| { - workspace::init( - app_state.clone(), - cx, - SqliteDb::open_in_memory("integration tests"), - ); + workspace::init(app_state.clone(), cx); call::init(client.clone(), user_store.clone(), cx); }); diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index f2542c9bc8..5af23b45d7 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -320,7 +320,7 @@ mod tests { use super::*; use editor::Editor; use gpui::TestAppContext; - use project::{Db, Project}; + use project::Project; use workspace::{AppState, Workspace}; #[test] @@ -345,7 +345,7 @@ mod tests { cx.update(|cx| { editor::init(cx); - workspace::init(app_state.clone(), cx, Db::open_in_memory("test")); + workspace::init(app_state.clone(), cx); init(cx); }); diff --git a/crates/db/src/db.rs b/crates/db/src/db.rs index 02fc51ee8d..56fc79f475 100644 --- a/crates/db/src/db.rs +++ b/crates/db/src/db.rs @@ -1,11 +1,12 @@ pub mod kvp; use std::fs; -use std::ops::Deref; use std::path::Path; +#[cfg(any(test, feature = "test-support"))] use anyhow::Result; use indoc::indoc; +#[cfg(any(test, feature = "test-support"))] use sqlez::connection::Connection; use sqlez::domain::Domain; use sqlez::thread_safe_connection::ThreadSafeConnection; @@ -17,47 +18,29 @@ const INITIALIZE_QUERY: &'static str = indoc! {" PRAGMA case_sensitive_like=TRUE; "}; -#[derive(Clone)] -pub struct Db(ThreadSafeConnection); +/// Open or create a database at the given directory path. +pub fn open_file_db() -> ThreadSafeConnection { + // Use 0 for now. Will implement incrementing and clearing of old db files soon TM + let current_db_dir = (*util::paths::DB_DIR).join(Path::new(&format!( + "0-{}", + *util::channel::RELEASE_CHANNEL_NAME + ))); + fs::create_dir_all(¤t_db_dir).expect("Should be able to create the database directory"); + let db_path = current_db_dir.join(Path::new("db.sqlite")); -impl Deref for Db { - type Target = sqlez::connection::Connection; - - fn deref(&self) -> &Self::Target { - &self.0.deref() - } + ThreadSafeConnection::new(db_path.to_string_lossy().as_ref(), true) + .with_initialize_query(INITIALIZE_QUERY) } -impl Db { - /// Open or create a database at the given directory path. - pub fn open(db_dir: &Path, channel: &'static str) -> Self { - // Use 0 for now. Will implement incrementing and clearing of old db files soon TM - let current_db_dir = db_dir.join(Path::new(&format!("0-{}", channel))); - fs::create_dir_all(¤t_db_dir) - .expect("Should be able to create the database directory"); - let db_path = current_db_dir.join(Path::new("db.sqlite")); - - Db( - ThreadSafeConnection::new(db_path.to_string_lossy().as_ref(), true) - .with_initialize_query(INITIALIZE_QUERY), - ) - } - - /// Open a in memory database for testing and as a fallback. - pub fn open_in_memory(db_name: &str) -> Self { - Db(ThreadSafeConnection::new(db_name, false).with_initialize_query(INITIALIZE_QUERY)) - } - - pub fn persisting(&self) -> bool { - self.persistent() - } - - pub fn write_to>(&self, dest: P) -> Result<()> { - let destination = Connection::open_file(dest.as_ref().to_string_lossy().as_ref()); - self.backup_main(&destination) - } - - pub fn open_as(&self) -> Db { - Db(self.0.for_domain()) - } +pub fn open_memory_db(db_name: &str) -> ThreadSafeConnection { + ThreadSafeConnection::new(db_name, false).with_initialize_query(INITIALIZE_QUERY) +} + +#[cfg(any(test, feature = "test-support"))] +pub fn write_db_to>( + conn: &ThreadSafeConnection, + dest: P, +) -> Result<()> { + let destination = Connection::open_file(dest.as_ref().to_string_lossy().as_ref()); + conn.backup_main(&destination) } diff --git a/crates/db/src/kvp.rs b/crates/db/src/kvp.rs index c5c9c1c5b5..1dd1cf69b7 100644 --- a/crates/db/src/kvp.rs +++ b/crates/db/src/kvp.rs @@ -1,7 +1,11 @@ -use super::Db; use anyhow::Result; use indoc::indoc; -use sqlez::{connection::Connection, domain::Domain, migrations::Migration}; + +use sqlez::{ + connection::Connection, domain::Domain, migrations::Migration, + thread_safe_connection::ThreadSafeConnection, +}; +use std::ops::Deref; pub(crate) const KVP_MIGRATION: Migration = Migration::new( "kvp", @@ -13,16 +17,29 @@ pub(crate) const KVP_MIGRATION: Migration = Migration::new( "}], ); -#[derive(Clone)] -pub enum KeyValue {} +lazy_static::lazy_static! { + pub static ref KEY_VALUE_STORE: KeyValueStore = + KeyValueStore(crate::open_file_db()); +} -impl Domain for KeyValue { +#[derive(Clone)] +pub struct KeyValueStore(ThreadSafeConnection); + +impl Domain for KeyValueStore { fn migrate(conn: &Connection) -> anyhow::Result<()> { KVP_MIGRATION.run(conn) } } -impl Db { +impl Deref for KeyValueStore { + type Target = ThreadSafeConnection; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl KeyValueStore { pub fn read_kvp(&self, key: &str) -> Result> { self.select_row_bound("SELECT value FROM kv_store WHERE key = (?)")?(key) } @@ -44,11 +61,11 @@ impl Db { mod tests { use anyhow::Result; - use super::*; + use crate::kvp::KeyValueStore; #[test] fn test_kvp() -> Result<()> { - let db = Db::open_in_memory("test_kvp"); + let db = KeyValueStore(crate::open_memory_db("test_kvp")); assert_eq!(db.read_kvp("key-1").unwrap(), None); diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 54fe5e46a2..683e3bdfcd 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -17,6 +17,7 @@ collections = { path = "../collections" } gpui_macros = { path = "../gpui_macros" } util = { path = "../util" } sum_tree = { path = "../sum_tree" } +sqlez = { path = "../sqlez" } async-task = "4.0.3" backtrace = { version = "0.3", optional = true } ctor = "0.1" diff --git a/crates/gpui/grammars/context-predicate/bindings/node/binding.cc b/crates/gpui/grammars/context-predicate/bindings/node/binding.cc index 9a3df4b028..1264f49100 100644 --- a/crates/gpui/grammars/context-predicate/bindings/node/binding.cc +++ b/crates/gpui/grammars/context-predicate/bindings/node/binding.cc @@ -1,10 +1,10 @@ +#include "nan.h" #include "tree_sitter/parser.h" #include -#include "nan.h" using namespace v8; -extern "C" TSLanguage * tree_sitter_context_predicate(); +extern "C" TSLanguage *tree_sitter_context_predicate(); namespace { @@ -16,13 +16,15 @@ void Init(Local exports, Local module) { tpl->InstanceTemplate()->SetInternalFieldCount(1); Local constructor = Nan::GetFunction(tpl).ToLocalChecked(); - Local instance = constructor->NewInstance(Nan::GetCurrentContext()).ToLocalChecked(); + Local instance = + constructor->NewInstance(Nan::GetCurrentContext()).ToLocalChecked(); Nan::SetInternalFieldPointer(instance, 0, tree_sitter_context_predicate()); - Nan::Set(instance, Nan::New("name").ToLocalChecked(), Nan::New("context_predicate").ToLocalChecked()); + Nan::Set(instance, Nan::New("name").ToLocalChecked(), + Nan::New("context_predicate").ToLocalChecked()); Nan::Set(module, Nan::New("exports").ToLocalChecked(), instance); } NODE_MODULE(tree_sitter_context_predicate_binding, Init) -} // namespace +} // namespace diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index 27cd2a1347..eb7554a39c 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -17,10 +17,15 @@ use crate::{ SceneBuilder, UpgradeModelHandle, UpgradeViewHandle, View, ViewHandle, WeakModelHandle, WeakViewHandle, }; +use anyhow::bail; use collections::{HashMap, HashSet}; use pathfinder_geometry::vector::{vec2f, Vector2F}; use serde_json::json; use smallvec::SmallVec; +use sqlez::{ + bindable::{Bind, Column}, + statement::Statement, +}; use std::{ marker::PhantomData, ops::{Deref, DerefMut, Range}, @@ -895,6 +900,31 @@ impl ToJson for Axis { } } +impl Bind for Axis { + fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result { + match self { + Axis::Horizontal => "Horizontal", + Axis::Vertical => "Vertical", + } + .bind(statement, start_index) + } +} + +impl Column for Axis { + fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> { + String::column(statement, start_index).and_then(|(axis_text, next_index)| { + Ok(( + match axis_text.as_str() { + "Horizontal" => Axis::Horizontal, + "Vertical" => Axis::Vertical, + _ => bail!("Stored serialized item kind is incorrect"), + }, + next_index, + )) + }) + } +} + pub trait Vector2FExt { fn along(self, axis: Axis) -> f32; } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index d1d8c96ce2..94558fee3e 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -63,7 +63,6 @@ use std::{ use thiserror::Error; use util::{defer, post_inc, ResultExt, TryFutureExt as _}; -pub use db::{kvp::KeyValue, Db}; pub use fs::*; pub use worktree::*; diff --git a/crates/settings/Cargo.toml b/crates/settings/Cargo.toml index ad184ad313..a292358e75 100644 --- a/crates/settings/Cargo.toml +++ b/crates/settings/Cargo.toml @@ -14,6 +14,7 @@ test-support = [] assets = { path = "../assets" } collections = { path = "../collections" } gpui = { path = "../gpui" } +sqlez = { path = "../sqlez" } fs = { path = "../fs" } anyhow = "1.0.38" futures = "0.3" diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index cb83c2c370..5137751579 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -2,7 +2,7 @@ mod keymap_file; pub mod settings_file; pub mod watched_json; -use anyhow::Result; +use anyhow::{bail, Result}; use gpui::{ font_cache::{FamilyId, FontCache}, AssetSource, @@ -14,6 +14,10 @@ use schemars::{ }; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_json::Value; +use sqlez::{ + bindable::{Bind, Column}, + statement::Statement, +}; use std::{collections::HashMap, fmt::Write as _, num::NonZeroU32, str, sync::Arc}; use theme::{Theme, ThemeRegistry}; use tree_sitter::Query; @@ -55,24 +59,6 @@ pub struct FeatureFlags { pub experimental_themes: bool, } -#[derive(Copy, Clone, PartialEq, Eq, Default)] -pub enum ReleaseChannel { - #[default] - Dev, - Preview, - Stable, -} - -impl ReleaseChannel { - pub fn name(&self) -> &'static str { - match self { - ReleaseChannel::Dev => "Zed Dev", - ReleaseChannel::Preview => "Zed Preview", - ReleaseChannel::Stable => "Zed", - } - } -} - impl FeatureFlags { pub fn keymap_files(&self) -> Vec<&'static str> { vec![] @@ -244,6 +230,33 @@ pub enum DockAnchor { Expanded, } +impl Bind for DockAnchor { + fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result { + match self { + DockAnchor::Bottom => "Bottom", + DockAnchor::Right => "Right", + DockAnchor::Expanded => "Expanded", + } + .bind(statement, start_index) + } +} + +impl Column for DockAnchor { + fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> { + String::column(statement, start_index).and_then(|(anchor_text, next_index)| { + Ok(( + match anchor_text.as_ref() { + "Bottom" => DockAnchor::Bottom, + "Right" => DockAnchor::Right, + "Expanded" => DockAnchor::Expanded, + _ => bail!("Stored dock anchor is incorrect"), + }, + next_index, + )) + }) + } +} + #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] pub struct SettingsFileContent { pub experiments: Option, diff --git a/crates/sqlez/src/statement.rs b/crates/sqlez/src/statement.rs index e0b284e628..b04f5bb82f 100644 --- a/crates/sqlez/src/statement.rs +++ b/crates/sqlez/src/statement.rs @@ -45,8 +45,8 @@ impl<'a> Statement<'a> { let sql = CString::new(query.as_ref())?; let mut remaining_sql = sql.as_c_str(); while { - let remaining_sql_str = remaining_sql.to_str()?; - remaining_sql_str.trim() != ";" && !remaining_sql_str.is_empty() + let remaining_sql_str = remaining_sql.to_str()?.trim(); + remaining_sql_str != ";" && !remaining_sql_str.is_empty() } { let mut raw_statement = 0 as *mut sqlite3_stmt; let mut remaining_sql_ptr = ptr::null(); diff --git a/crates/sqlez/src/thread_safe_connection.rs b/crates/sqlez/src/thread_safe_connection.rs index 1081101f6a..b9bb1657ea 100644 --- a/crates/sqlez/src/thread_safe_connection.rs +++ b/crates/sqlez/src/thread_safe_connection.rs @@ -13,6 +13,9 @@ pub struct ThreadSafeConnection { _pd: PhantomData, } +unsafe impl Send for ThreadSafeConnection {} +unsafe impl Sync for ThreadSafeConnection {} + impl ThreadSafeConnection { pub fn new(uri: &str, persistent: bool) -> Self { Self { diff --git a/crates/util/src/channel.rs b/crates/util/src/channel.rs new file mode 100644 index 0000000000..ab5b53b4ab --- /dev/null +++ b/crates/util/src/channel.rs @@ -0,0 +1,32 @@ +use std::env; + +use lazy_static::lazy_static; + +lazy_static! { + pub static ref RELEASE_CHANNEL_NAME: String = env::var("ZED_RELEASE_CHANNEL") + .unwrap_or(include_str!("../../zed/RELEASE_CHANNEL").to_string()); + pub static ref RELEASE_CHANNEL: ReleaseChannel = match RELEASE_CHANNEL_NAME.as_str() { + "dev" => ReleaseChannel::Dev, + "preview" => ReleaseChannel::Preview, + "stable" => ReleaseChannel::Stable, + _ => panic!("invalid release channel {}", *RELEASE_CHANNEL_NAME), + }; +} + +#[derive(Copy, Clone, PartialEq, Eq, Default)] +pub enum ReleaseChannel { + #[default] + Dev, + Preview, + Stable, +} + +impl ReleaseChannel { + pub fn name(&self) -> &'static str { + match self { + ReleaseChannel::Dev => "Zed Dev", + ReleaseChannel::Preview => "Zed Preview", + ReleaseChannel::Stable => "Zed", + } + } +} diff --git a/crates/util/src/lib.rs b/crates/util/src/lib.rs index b03bc21210..78536f01d0 100644 --- a/crates/util/src/lib.rs +++ b/crates/util/src/lib.rs @@ -1,3 +1,4 @@ +pub mod channel; pub mod paths; #[cfg(any(test, feature = "test-support"))] pub mod test; diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index f8bcba5eb7..553479b175 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -36,6 +36,7 @@ util = { path = "../util" } bincode = "1.2.1" anyhow = "1.0.38" futures = "0.3" +lazy_static = "1.4" log = { version = "0.4.16", features = ["kv_unstable_serde"] } parking_lot = "0.11.1" postage = { version = "0.4.1", features = ["futures-traits"] } diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs new file mode 100644 index 0000000000..8a80dc5a76 --- /dev/null +++ b/crates/workspace/src/persistence.rs @@ -0,0 +1,494 @@ +#![allow(dead_code)] + +pub mod model; + +use std::ops::Deref; +use std::path::{Path, PathBuf}; + +use anyhow::{bail, Context, Result}; +use db::open_file_db; +use gpui::Axis; +use indoc::indoc; +use lazy_static::lazy_static; + +use sqlez::thread_safe_connection::ThreadSafeConnection; +use sqlez::{connection::Connection, domain::Domain, migrations::Migration}; +use util::{iife, unzip_option, ResultExt}; + +use super::Workspace; + +use model::{ + GroupId, PaneId, SerializedItem, SerializedItemKind, SerializedPane, SerializedPaneGroup, + SerializedWorkspace, WorkspaceId, +}; + +lazy_static! { + pub static ref DB: WorkspaceDb = WorkspaceDb(open_file_db()); +} + +pub struct WorkspaceDb(ThreadSafeConnection); + +impl Deref for WorkspaceDb { + type Target = ThreadSafeConnection; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +pub(crate) const WORKSPACES_MIGRATION: Migration = Migration::new( + "workspace", + &[indoc! {" + CREATE TABLE workspaces( + workspace_id BLOB PRIMARY KEY, + dock_anchor TEXT, -- Enum: 'Bottom' / 'Right' / 'Expanded' + dock_visible INTEGER, -- Boolean + timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL + ) STRICT; + + CREATE TABLE pane_groups( + group_id INTEGER PRIMARY KEY, + workspace_id BLOB NOT NULL, + parent_group_id INTEGER, -- NULL indicates that this is a root node + position INTEGER, -- NULL indicates that this is a root node + axis TEXT NOT NULL, -- Enum: 'Vertical' / 'Horizontal' + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE, + FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE + ) STRICT; + + CREATE TABLE panes( + pane_id INTEGER PRIMARY KEY, + workspace_id BLOB NOT NULL, + parent_group_id INTEGER, -- NULL, this is a dock pane + position INTEGER, -- NULL, this is a dock pane + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE, + FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE + ) STRICT; + + CREATE TABLE items( + item_id INTEGER NOT NULL, -- This is the item's view id, so this is not unique + workspace_id BLOB NOT NULL, + pane_id INTEGER NOT NULL, + kind TEXT NOT NULL, + position INTEGER NOT NULL, + FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE + FOREIGN KEY(pane_id) REFERENCES panes(pane_id) ON DELETE CASCADE + PRIMARY KEY(item_id, workspace_id) + ) STRICT; + "}], +); + +impl Domain for Workspace { + fn migrate(conn: &Connection) -> anyhow::Result<()> { + WORKSPACES_MIGRATION.run(&conn) + } +} + +impl WorkspaceDb { + /// Returns a serialized workspace for the given worktree_roots. If the passed array + /// is empty, the most recent workspace is returned instead. If no workspace for the + /// passed roots is stored, returns none. + pub fn workspace_for_roots>( + &self, + worktree_roots: &[P], + ) -> Option { + let workspace_id: WorkspaceId = 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_anchor, dock_visible) = iife!({ + if worktree_roots.len() == 0 { + self.select_row(indoc! {" + SELECT workspace_id, dock_anchor, dock_visible + FROM workspaces + ORDER BY timestamp DESC LIMIT 1"})?()? + } else { + self.select_row_bound(indoc! {" + SELECT workspace_id, dock_anchor, dock_visible + FROM workspaces + WHERE workspace_id = ?"})?(&workspace_id)? + } + .context("No workspaces found") + }) + .warn_on_err() + .flatten()?; + + Some(SerializedWorkspace { + dock_pane: self + .get_dock_pane(&workspace_id) + .context("Getting dock pane") + .log_err()?, + center_group: self + .get_center_pane_group(&workspace_id) + .context("Getting center group") + .log_err()?, + dock_anchor, + dock_visible, + }) + } + + /// Saves a workspace using the worktree roots. Will garbage collect any workspaces + /// that used this workspace previously + pub fn save_workspace>( + &self, + worktree_roots: &[P], + old_roots: Option<&[P]>, + workspace: &SerializedWorkspace, + ) { + let workspace_id: WorkspaceId = worktree_roots.into(); + + self.with_savepoint("update_worktrees", || { + if let Some(old_roots) = old_roots { + let old_id: WorkspaceId = old_roots.into(); + + self.exec_bound("DELETE FROM WORKSPACES WHERE workspace_id = ?")?(&old_id)?; + } + + // Delete any previous workspaces with the same roots. This cascades to all + // other tables that are based on the same roots set. + // Insert new workspace into workspaces table if none were found + self.exec_bound("DELETE FROM workspaces WHERE workspace_id = ?;")?(&workspace_id)?; + + self.exec_bound( + "INSERT INTO workspaces(workspace_id, dock_anchor, dock_visible) VALUES (?, ?, ?)", + )?((&workspace_id, workspace.dock_anchor, workspace.dock_visible))?; + + // Save center pane group and dock pane + self.save_pane_group(&workspace_id, &workspace.center_group, None)?; + self.save_pane(&workspace_id, &workspace.dock_pane, None)?; + + Ok(()) + }) + .with_context(|| { + format!( + "Update workspace with roots {:?}", + worktree_roots + .iter() + .map(|p| p.as_ref()) + .collect::>() + ) + }) + .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> { + iife!({ + // TODO, upgrade anyhow: https://docs.rs/anyhow/1.0.66/anyhow/fn.Ok.html + Ok::<_, anyhow::Error>( + self.select_bound::( + "SELECT workspace_id FROM workspaces ORDER BY timestamp DESC LIMIT ?", + )?(limit)? + .into_iter() + .map(|id| id.paths()) + .collect::>>(), + ) + }) + .log_err() + .unwrap_or_default() + } + + pub(crate) fn get_center_pane_group( + &self, + workspace_id: &WorkspaceId, + ) -> Result { + self.get_pane_group_children(workspace_id, None)? + .into_iter() + .next() + .context("No center pane group") + } + + fn get_pane_group_children<'a>( + &self, + workspace_id: &WorkspaceId, + group_id: Option, + ) -> Result> { + self.select_bound::<(Option, &WorkspaceId), (Option, Option, Option)>(indoc! {" + SELECT group_id, axis, pane_id + FROM (SELECT group_id, axis, NULL as pane_id, position, parent_group_id, workspace_id + FROM pane_groups + UNION + SELECT NULL, NULL, pane_id, position, parent_group_id, workspace_id + FROM panes + -- Remove the dock panes from the union + WHERE parent_group_id IS NOT NULL and position IS NOT NULL) + WHERE parent_group_id IS ? AND workspace_id = ? + ORDER BY position + "})?((group_id, workspace_id))? + .into_iter() + .map(|(group_id, axis, pane_id)| { + if let Some((group_id, axis)) = group_id.zip(axis) { + Ok(SerializedPaneGroup::Group { + axis, + children: self.get_pane_group_children( + workspace_id, + Some(group_id), + )?, + }) + } else if let Some(pane_id) = pane_id { + Ok(SerializedPaneGroup::Pane(SerializedPane { + children: self.get_items( pane_id)?, + })) + } else { + bail!("Pane Group Child was neither a pane group or a pane"); + } + }) + .collect::>() + } + + pub(crate) fn save_pane_group( + &self, + workspace_id: &WorkspaceId, + pane_group: &SerializedPaneGroup, + parent: Option<(GroupId, usize)>, + ) -> Result<()> { + if parent.is_none() && !matches!(pane_group, SerializedPaneGroup::Group { .. }) { + bail!("Pane groups must have a SerializedPaneGroup::Group at the root") + } + + let (parent_id, position) = unzip_option(parent); + + match pane_group { + SerializedPaneGroup::Group { axis, children } => { + let parent_id = self.insert_bound("INSERT INTO pane_groups(workspace_id, parent_group_id, position, axis) VALUES (?, ?, ?, ?)")? + ((workspace_id, parent_id, position, *axis))?; + + for (position, group) in children.iter().enumerate() { + self.save_pane_group(workspace_id, group, Some((parent_id, position)))? + } + Ok(()) + } + SerializedPaneGroup::Pane(pane) => self.save_pane(workspace_id, pane, parent), + } + } + + pub(crate) fn get_dock_pane(&self, workspace_id: &WorkspaceId) -> Result { + let pane_id = self.select_row_bound(indoc! {" + SELECT pane_id FROM panes + WHERE workspace_id = ? AND parent_group_id IS NULL AND position IS NULL"})?( + workspace_id, + )? + .context("No dock pane for workspace")?; + + Ok(SerializedPane::new( + self.get_items(pane_id).context("Reading items")?, + )) + } + + pub(crate) fn save_pane( + &self, + workspace_id: &WorkspaceId, + pane: &SerializedPane, + parent: Option<(GroupId, usize)>, + ) -> Result<()> { + let (parent_id, order) = unzip_option(parent); + + let pane_id = self.insert_bound( + "INSERT INTO panes(workspace_id, parent_group_id, position) VALUES (?, ?, ?)", + )?((workspace_id, parent_id, order))?; + + self.save_items(workspace_id, pane_id, &pane.children) + .context("Saving items") + } + + pub(crate) fn get_items(&self, pane_id: PaneId) -> Result> { + Ok(self.select_bound(indoc! {" + SELECT item_id, kind FROM items + WHERE pane_id = ? + ORDER BY position"})?(pane_id)? + .into_iter() + .map(|(item_id, kind)| match kind { + SerializedItemKind::Terminal => SerializedItem::Terminal { item_id }, + _ => unimplemented!(), + }) + .collect()) + } + + pub(crate) fn save_items( + &self, + workspace_id: &WorkspaceId, + pane_id: PaneId, + items: &[SerializedItem], + ) -> Result<()> { + let mut delete_old = self + .exec_bound("DELETE FROM items WHERE workspace_id = ? AND pane_id = ? AND item_id = ?") + .context("Preparing deletion")?; + let mut insert_new = self.exec_bound( + "INSERT INTO items(item_id, workspace_id, pane_id, kind, position) VALUES (?, ?, ?, ?, ?)", + ).context("Preparing insertion")?; + for (position, item) in items.iter().enumerate() { + delete_old((workspace_id, pane_id, item.item_id()))?; + insert_new((item.item_id(), workspace_id, pane_id, item.kind(), position))?; + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use db::open_memory_db; + use settings::DockAnchor; + + use super::*; + + #[test] + fn test_workspace_assignment() { + // env_logger::try_init().ok(); + + let db = WorkspaceDb(open_memory_db("test_basic_functionality")); + + let workspace_1 = SerializedWorkspace { + dock_anchor: DockAnchor::Bottom, + dock_visible: true, + center_group: Default::default(), + dock_pane: Default::default(), + }; + + let workspace_2 = SerializedWorkspace { + dock_anchor: DockAnchor::Expanded, + dock_visible: false, + center_group: Default::default(), + dock_pane: Default::default(), + }; + + let workspace_3 = SerializedWorkspace { + dock_anchor: DockAnchor::Right, + dock_visible: true, + center_group: Default::default(), + dock_pane: Default::default(), + }; + + db.save_workspace(&["/tmp", "/tmp2"], None, &workspace_1); + db.save_workspace(&["/tmp"], None, &workspace_2); + + db::write_db_to(&db, "test.db").unwrap(); + + // Test that paths are treated as a set + assert_eq!( + db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(), + workspace_1 + ); + assert_eq!( + db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(), + workspace_1 + ); + + // Make sure that other keys work + assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2); + assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None); + + // Test 'mutate' case of updating a pre-existing id + db.save_workspace(&["/tmp", "/tmp2"], Some(&["/tmp", "/tmp2"]), &workspace_2); + assert_eq!( + db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(), + workspace_2 + ); + + // Test other mechanism for mutating + db.save_workspace(&["/tmp", "/tmp2"], None, &workspace_3); + assert_eq!( + db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(), + workspace_3 + ); + + // Make sure that updating paths differently also works + db.save_workspace( + &["/tmp3", "/tmp4", "/tmp2"], + Some(&["/tmp", "/tmp2"]), + &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::persistence::model::SerializedWorkspace; + use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup}; + + fn default_workspace( + dock_pane: SerializedPane, + center_group: &SerializedPaneGroup, + ) -> SerializedWorkspace { + SerializedWorkspace { + dock_anchor: DockAnchor::Right, + dock_visible: false, + center_group: center_group.clone(), + dock_pane, + } + } + + #[test] + fn test_basic_dock_pane() { + // env_logger::try_init().ok(); + + let db = WorkspaceDb(open_memory_db("basic_dock_pane")); + + let dock_pane = crate::persistence::model::SerializedPane { + children: vec![ + SerializedItem::Terminal { item_id: 1 }, + SerializedItem::Terminal { item_id: 4 }, + SerializedItem::Terminal { item_id: 2 }, + SerializedItem::Terminal { item_id: 3 }, + ], + }; + + let workspace = default_workspace(dock_pane, &Default::default()); + + db.save_workspace(&["/tmp"], None, &workspace); + + let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap(); + + assert_eq!(workspace.dock_pane, new_workspace.dock_pane); + } + + #[test] + fn test_simple_split() { + // env_logger::try_init().ok(); + + let db = WorkspaceDb(open_memory_db("simple_split")); + + // ----------------- + // | 1,2 | 5,6 | + // | - - - | | + // | 3,4 | | + // ----------------- + let center_pane = SerializedPaneGroup::Group { + axis: gpui::Axis::Horizontal, + children: vec![ + SerializedPaneGroup::Group { + axis: gpui::Axis::Vertical, + children: vec![ + SerializedPaneGroup::Pane(SerializedPane { + children: vec![ + SerializedItem::Terminal { item_id: 1 }, + SerializedItem::Terminal { item_id: 2 }, + ], + }), + SerializedPaneGroup::Pane(SerializedPane { + children: vec![ + SerializedItem::Terminal { item_id: 4 }, + SerializedItem::Terminal { item_id: 3 }, + ], + }), + ], + }, + SerializedPaneGroup::Pane(SerializedPane { + children: vec![ + SerializedItem::Terminal { item_id: 5 }, + SerializedItem::Terminal { item_id: 6 }, + ], + }), + ], + }; + + let workspace = default_workspace(Default::default(), ¢er_pane); + + db.save_workspace(&["/tmp"], None, &workspace); + + assert_eq!(workspace.center_group, center_pane); + } +} diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs new file mode 100644 index 0000000000..824f649f98 --- /dev/null +++ b/crates/workspace/src/persistence/model.rs @@ -0,0 +1,188 @@ +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; + +use anyhow::{bail, Result}; + +use gpui::Axis; +use settings::DockAnchor; +use sqlez::{ + bindable::{Bind, Column}, + statement::Statement, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct WorkspaceId(Vec); + +impl WorkspaceId { + pub fn paths(self) -> Vec { + self.0 + } +} + +impl, T: IntoIterator> From for WorkspaceId { + fn from(iterator: T) -> Self { + let mut roots = iterator + .into_iter() + .map(|p| p.as_ref().to_path_buf()) + .collect::>(); + roots.sort(); + Self(roots) + } +} + +impl Bind for &WorkspaceId { + fn bind(&self, statement: &Statement, start_index: i32) -> Result { + bincode::serialize(&self.0) + .expect("Bincode serialization of paths should not fail") + .bind(statement, start_index) + } +} + +impl Column for WorkspaceId { + 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)) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct SerializedWorkspace { + pub dock_anchor: DockAnchor, + pub dock_visible: bool, + pub center_group: SerializedPaneGroup, + pub dock_pane: SerializedPane, +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum SerializedPaneGroup { + Group { + axis: Axis, + children: Vec, + }, + Pane(SerializedPane), +} + +impl Default for SerializedPaneGroup { + fn default() -> Self { + Self::Group { + axis: Axis::Horizontal, + children: vec![Self::Pane(Default::default())], + } + } +} + +#[derive(Debug, PartialEq, Eq, Default, Clone)] +pub struct SerializedPane { + pub(crate) children: Vec, +} + +impl SerializedPane { + pub fn new(children: Vec) -> Self { + SerializedPane { children } + } +} + +pub type GroupId = i64; +pub type PaneId = i64; +pub type ItemId = usize; + +pub(crate) enum SerializedItemKind { + Editor, + Diagnostics, + ProjectSearch, + Terminal, +} + +impl Bind for SerializedItemKind { + fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result { + match self { + SerializedItemKind::Editor => "Editor", + SerializedItemKind::Diagnostics => "Diagnostics", + SerializedItemKind::ProjectSearch => "ProjectSearch", + SerializedItemKind::Terminal => "Terminal", + } + .bind(statement, start_index) + } +} + +impl Column for SerializedItemKind { + fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> { + String::column(statement, start_index).and_then(|(kind_text, next_index)| { + Ok(( + match kind_text.as_ref() { + "Editor" => SerializedItemKind::Editor, + "Diagnostics" => SerializedItemKind::Diagnostics, + "ProjectSearch" => SerializedItemKind::ProjectSearch, + "Terminal" => SerializedItemKind::Terminal, + _ => bail!("Stored serialized item kind is incorrect"), + }, + next_index, + )) + }) + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum SerializedItem { + Editor { item_id: usize, path: Arc }, + Diagnostics { item_id: usize }, + ProjectSearch { item_id: usize, query: String }, + Terminal { item_id: usize }, +} + +impl SerializedItem { + pub fn item_id(&self) -> usize { + match self { + SerializedItem::Editor { item_id, .. } => *item_id, + SerializedItem::Diagnostics { item_id } => *item_id, + SerializedItem::ProjectSearch { item_id, .. } => *item_id, + SerializedItem::Terminal { item_id } => *item_id, + } + } + + pub(crate) fn kind(&self) -> SerializedItemKind { + match self { + SerializedItem::Editor { .. } => SerializedItemKind::Editor, + SerializedItem::Diagnostics { .. } => SerializedItemKind::Diagnostics, + SerializedItem::ProjectSearch { .. } => SerializedItemKind::ProjectSearch, + SerializedItem::Terminal { .. } => SerializedItemKind::Terminal, + } + } +} + +#[cfg(test)] +mod tests { + use sqlez::connection::Connection; + + use crate::persistence::model::DockAnchor; + + use super::WorkspaceId; + + #[test] + fn test_workspace_round_trips() { + let db = Connection::open_memory("workspace_id_round_trips"); + + db.exec(indoc::indoc! {" + CREATE TABLE workspace_id_test( + workspace_id BLOB, + dock_anchor TEXT + );"}) + .unwrap()() + .unwrap(); + + let workspace_id: WorkspaceId = WorkspaceId::from(&["\test2", "\test1"]); + + db.exec_bound("INSERT INTO workspace_id_test(workspace_id, dock_anchor) VALUES (?,?)") + .unwrap()((&workspace_id, DockAnchor::Bottom)) + .unwrap(); + + assert_eq!( + db.select_row("SELECT workspace_id, dock_anchor FROM workspace_id_test LIMIT 1") + .unwrap()() + .unwrap(), + Some((WorkspaceId::from(&["\test1", "\test2"]), DockAnchor::Bottom)) + ); + } +} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 39843859c0..085d9e2eb2 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -5,19 +5,18 @@ pub mod dock; pub mod pane; pub mod pane_group; +mod persistence; pub mod searchable; pub mod shared_screen; pub mod sidebar; mod status_bar; mod toolbar; -mod workspace_db; -use crate::workspace_db::model::SerializedWorkspace; +use crate::persistence::model::SerializedWorkspace; use anyhow::{anyhow, Context, Result}; use call::ActiveCall; use client::{proto, Client, PeerId, TypedEnvelope, UserStore}; use collections::{hash_map, HashMap, HashSet}; -use db::{kvp::KeyValue, Db}; use dock::{DefaultItemFactory, Dock, ToggleDockButton}; use drag_and_drop::DragAndDrop; use fs::{self, Fs}; @@ -165,9 +164,7 @@ impl_internal_actions!( ); impl_actions!(workspace, [ActivatePane]); -pub fn init(app_state: Arc, cx: &mut MutableAppContext, db: Db) { - cx.set_global(db); - +pub fn init(app_state: Arc, cx: &mut MutableAppContext) { pane::init(cx); dock::init(cx); @@ -1291,12 +1288,8 @@ impl Workspace { } // Use the resolved worktree roots to get the serialized_db from the database - let serialized_workspace = cx.read(|cx| { - Workspace::workspace_for_roots( - cx.global::>(), - &Vec::from_iter(worktree_roots.into_iter())[..], - ) - }); + let serialized_workspace = persistence::DB + .workspace_for_roots(&Vec::from_iter(worktree_roots.into_iter())[..]); // Use the serialized workspace to construct the new window let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| { diff --git a/crates/workspace/src/workspace_db.rs b/crates/workspace/src/workspace_db.rs deleted file mode 100644 index e896dd6c27..0000000000 --- a/crates/workspace/src/workspace_db.rs +++ /dev/null @@ -1,765 +0,0 @@ -use anyhow::{bail, Context, Result}; - -use db::Db; -use util::{iife, unzip_option, ResultExt}; - -use std::path::{Path, PathBuf}; - -use indoc::indoc; -use sqlez::{connection::Connection, domain::Domain, migrations::Migration}; - -use super::Workspace; - -use self::model::{ - Axis, GroupId, PaneId, SerializedItem, SerializedItemKind, SerializedPane, SerializedPaneGroup, - SerializedWorkspace, WorkspaceId, -}; - -// 1) Move all of this into Workspace crate -// 2) Deserialize items fully -// 3) Typed prepares (including how you expect to pull data out) -// 4) Investigate Tree column impls - -pub(crate) const WORKSPACES_MIGRATION: Migration = Migration::new( - "workspace", - &[indoc! {" - CREATE TABLE workspaces( - workspace_id BLOB PRIMARY KEY, - dock_anchor TEXT, -- Enum: 'Bottom' / 'Right' / 'Expanded' - dock_visible INTEGER, -- Boolean - timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL - ) STRICT; - "}], -); - -pub(crate) const PANE_MIGRATIONS: Migration = Migration::new( - "pane", - &[indoc! {" - CREATE TABLE pane_groups( - group_id INTEGER PRIMARY KEY, - workspace_id BLOB NOT NULL, - parent_group_id INTEGER, -- NULL indicates that this is a root node - position INTEGER, -- NULL indicates that this is a root node - axis TEXT NOT NULL, -- Enum: 'Vertical' / 'Horizontal' - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE, - FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE - ) STRICT; - - CREATE TABLE panes( - pane_id INTEGER PRIMARY KEY, - workspace_id BLOB NOT NULL, - parent_group_id INTEGER, -- NULL, this is a dock pane - position INTEGER, -- NULL, this is a dock pane - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE, - FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE - ) STRICT; - "}], -); - -pub(crate) const ITEM_MIGRATIONS: Migration = Migration::new( - "item", - &[indoc! {" - CREATE TABLE items( - item_id INTEGER NOT NULL, -- This is the item's view id, so this is not unique - workspace_id BLOB NOT NULL, - pane_id INTEGER NOT NULL, - kind TEXT NOT NULL, - position INTEGER NOT NULL, - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE - FOREIGN KEY(pane_id) REFERENCES panes(pane_id) ON DELETE CASCADE - PRIMARY KEY(item_id, workspace_id) - ) STRICT; - "}], -); - -impl Domain for Workspace { - fn migrate(conn: &Connection) -> anyhow::Result<()> { - WORKSPACES_MIGRATION.run(&conn)?; - PANE_MIGRATIONS.run(&conn)?; - ITEM_MIGRATIONS.run(&conn) - } -} - -impl Workspace { - /// Returns a serialized workspace for the given worktree_roots. If the passed array - /// is empty, the most recent workspace is returned instead. If no workspace for the - /// passed roots is stored, returns none. - pub fn workspace_for_roots>( - db: &Db, - worktree_roots: &[P], - ) -> Option { - let workspace_id: WorkspaceId = 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_anchor, dock_visible) = iife!({ - if worktree_roots.len() == 0 { - db.select_row(indoc! {" - SELECT workspace_id, dock_anchor, dock_visible - FROM workspaces - ORDER BY timestamp DESC LIMIT 1"})?()? - } else { - db.select_row_bound(indoc! {" - SELECT workspace_id, dock_anchor, dock_visible - FROM workspaces - WHERE workspace_id = ?"})?(&workspace_id)? - } - .context("No workspaces found") - }) - .warn_on_err() - .flatten()?; - - Some(SerializedWorkspace { - dock_pane: Workspace::get_dock_pane(&db, &workspace_id) - .context("Getting dock pane") - .log_err()?, - center_group: Workspace::get_center_pane_group(&db, &workspace_id) - .context("Getting center group") - .log_err()?, - dock_anchor, - dock_visible, - }) - } - - /// Saves a workspace using the worktree roots. Will garbage collect any workspaces - /// that used this workspace previously - pub fn save_workspace>( - db: &Db, - worktree_roots: &[P], - old_roots: Option<&[P]>, - workspace: &SerializedWorkspace, - ) { - let workspace_id: WorkspaceId = worktree_roots.into(); - - db.with_savepoint("update_worktrees", || { - if let Some(old_roots) = old_roots { - let old_id: WorkspaceId = old_roots.into(); - - db.exec_bound("DELETE FROM WORKSPACES WHERE workspace_id = ?")?(&old_id)?; - } - - // Delete any previous workspaces with the same roots. This cascades to all - // other tables that are based on the same roots set. - // Insert new workspace into workspaces table if none were found - db.exec_bound("DELETE FROM workspaces WHERE workspace_id = ?;")?(&workspace_id)?; - - db.exec_bound( - "INSERT INTO workspaces(workspace_id, dock_anchor, dock_visible) VALUES (?, ?, ?)", - )?((&workspace_id, workspace.dock_anchor, workspace.dock_visible))?; - - // Save center pane group and dock pane - Workspace::save_pane_group(db, &workspace_id, &workspace.center_group, None)?; - Workspace::save_pane(db, &workspace_id, &workspace.dock_pane, None)?; - - Ok(()) - }) - .with_context(|| { - format!( - "Update workspace with roots {:?}", - worktree_roots - .iter() - .map(|p| p.as_ref()) - .collect::>() - ) - }) - .log_err(); - } - - /// Returns the previous workspace ids sorted by last modified along with their opened worktree roots - pub fn recent_workspaces(conn: &Connection, limit: usize) -> Vec> { - iife!({ - // TODO, upgrade anyhow: https://docs.rs/anyhow/1.0.66/anyhow/fn.Ok.html - Ok::<_, anyhow::Error>( - conn.select_bound::( - "SELECT workspace_id FROM workspaces ORDER BY timestamp DESC LIMIT ?", - )?(limit)? - .into_iter() - .map(|id| id.paths()) - .collect::>>(), - ) - }) - .log_err() - .unwrap_or_default() - } - - pub(crate) fn get_center_pane_group( - db: &Db, - workspace_id: &WorkspaceId, - ) -> Result { - Workspace::get_pane_group_children(&db, workspace_id, None)? - .into_iter() - .next() - .context("No center pane group") - } - - fn get_pane_group_children<'a>( - db: &Db, - workspace_id: &WorkspaceId, - group_id: Option, - ) -> Result> { - db.select_bound::<(Option, &WorkspaceId), (Option, Option, Option)>(indoc! {" - SELECT group_id, axis, pane_id - FROM (SELECT group_id, axis, NULL as pane_id, position, parent_group_id, workspace_id - FROM pane_groups - UNION - SELECT NULL, NULL, pane_id, position, parent_group_id, workspace_id - FROM panes - -- Remove the dock panes from the union - WHERE parent_group_id IS NOT NULL and position IS NOT NULL) - WHERE parent_group_id IS ? AND workspace_id = ? - ORDER BY position - "})?((group_id, workspace_id))? - .into_iter() - .map(|(group_id, axis, pane_id)| { - if let Some((group_id, axis)) = group_id.zip(axis) { - Ok(SerializedPaneGroup::Group { - axis, - children: Workspace::get_pane_group_children( - db, - workspace_id, - Some(group_id), - )?, - }) - } else if let Some(pane_id) = pane_id { - Ok(SerializedPaneGroup::Pane(SerializedPane { - children: Workspace::get_items(db, pane_id)?, - })) - } else { - bail!("Pane Group Child was neither a pane group or a pane"); - } - }) - .collect::>() - } - - pub(crate) fn save_pane_group( - db: &Db, - workspace_id: &WorkspaceId, - pane_group: &SerializedPaneGroup, - parent: Option<(GroupId, usize)>, - ) -> Result<()> { - if parent.is_none() && !matches!(pane_group, SerializedPaneGroup::Group { .. }) { - bail!("Pane groups must have a SerializedPaneGroup::Group at the root") - } - - let (parent_id, position) = unzip_option(parent); - - match pane_group { - SerializedPaneGroup::Group { axis, children } => { - let parent_id = db.insert_bound("INSERT INTO pane_groups(workspace_id, parent_group_id, position, axis) VALUES (?, ?, ?, ?)")? - ((workspace_id, parent_id, position, *axis))?; - - for (position, group) in children.iter().enumerate() { - Workspace::save_pane_group( - db, - workspace_id, - group, - Some((parent_id, position)), - )? - } - Ok(()) - } - SerializedPaneGroup::Pane(pane) => Workspace::save_pane(db, workspace_id, pane, parent), - } - } - - pub(crate) fn get_dock_pane( - db: &Db, - workspace_id: &WorkspaceId, - ) -> Result { - let pane_id = db.select_row_bound(indoc! {" - SELECT pane_id FROM panes - WHERE workspace_id = ? AND parent_group_id IS NULL AND position IS NULL"})?( - workspace_id, - )? - .context("No dock pane for workspace")?; - - Ok(SerializedPane::new( - Workspace::get_items(db, pane_id).context("Reading items")?, - )) - } - - pub(crate) fn save_pane( - db: &Db, - workspace_id: &WorkspaceId, - pane: &SerializedPane, - parent: Option<(GroupId, usize)>, - ) -> Result<()> { - let (parent_id, order) = unzip_option(parent); - - let pane_id = db.insert_bound( - "INSERT INTO panes(workspace_id, parent_group_id, position) VALUES (?, ?, ?)", - )?((workspace_id, parent_id, order))?; - - Workspace::save_items(db, workspace_id, pane_id, &pane.children).context("Saving items") - } - - pub(crate) fn get_items(db: &Db, pane_id: PaneId) -> Result> { - Ok(db.select_bound(indoc! {" - SELECT item_id, kind FROM items - WHERE pane_id = ? - ORDER BY position"})?(pane_id)? - .into_iter() - .map(|(item_id, kind)| match kind { - SerializedItemKind::Terminal => SerializedItem::Terminal { item_id }, - _ => unimplemented!(), - }) - .collect()) - } - - pub(crate) fn save_items( - db: &Db, - workspace_id: &WorkspaceId, - pane_id: PaneId, - items: &[SerializedItem], - ) -> Result<()> { - let mut delete_old = db - .exec_bound("DELETE FROM items WHERE workspace_id = ? AND pane_id = ? AND item_id = ?") - .context("Preparing deletion")?; - let mut insert_new = db.exec_bound( - "INSERT INTO items(item_id, workspace_id, pane_id, kind, position) VALUES (?, ?, ?, ?, ?)", - ).context("Preparing insertion")?; - for (position, item) in items.iter().enumerate() { - delete_old((workspace_id, pane_id, item.item_id()))?; - insert_new((item.item_id(), workspace_id, pane_id, item.kind(), position))?; - } - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use crate::workspace_db::model::DockAnchor::{Bottom, Expanded, Right}; - use crate::{Db, Workspace}; - - #[test] - fn test_workspace_assignment() { - // env_logger::try_init().ok(); - - let db = Db::open_in_memory("test_basic_functionality"); - - let workspace_1 = SerializedWorkspace { - dock_anchor: Bottom, - dock_visible: true, - center_group: Default::default(), - dock_pane: Default::default(), - }; - - let workspace_2 = SerializedWorkspace { - dock_anchor: Expanded, - dock_visible: false, - center_group: Default::default(), - dock_pane: Default::default(), - }; - - let workspace_3 = SerializedWorkspace { - dock_anchor: Right, - dock_visible: true, - center_group: Default::default(), - dock_pane: Default::default(), - }; - - Workspace::save_workspace(&db, &["/tmp", "/tmp2"], None, &workspace_1); - Workspace::save_workspace(&db, &["/tmp"], None, &workspace_2); - - db.write_to("test.db").unwrap(); - - // Test that paths are treated as a set - assert_eq!( - Workspace::workspace_for_roots(&db, &["/tmp", "/tmp2"]).unwrap(), - workspace_1 - ); - assert_eq!( - Workspace::workspace_for_roots(&db, &["/tmp2", "/tmp"]).unwrap(), - workspace_1 - ); - - // Make sure that other keys work - assert_eq!( - Workspace::workspace_for_roots(&db, &["/tmp"]).unwrap(), - workspace_2 - ); - assert_eq!( - Workspace::workspace_for_roots(&db, &["/tmp3", "/tmp2", "/tmp4"]), - None - ); - - // Test 'mutate' case of updating a pre-existing id - Workspace::save_workspace( - &db, - &["/tmp", "/tmp2"], - Some(&["/tmp", "/tmp2"]), - &workspace_2, - ); - assert_eq!( - Workspace::workspace_for_roots(&db, &["/tmp", "/tmp2"]).unwrap(), - workspace_2 - ); - - // Test other mechanism for mutating - Workspace::save_workspace(&db, &["/tmp", "/tmp2"], None, &workspace_3); - assert_eq!( - Workspace::workspace_for_roots(&db, &["/tmp", "/tmp2"]).unwrap(), - workspace_3 - ); - - // Make sure that updating paths differently also works - Workspace::save_workspace( - &db, - &["/tmp3", "/tmp4", "/tmp2"], - Some(&["/tmp", "/tmp2"]), - &workspace_3, - ); - assert_eq!(Workspace::workspace_for_roots(&db, &["/tmp2", "tmp"]), None); - assert_eq!( - Workspace::workspace_for_roots(&db, &["/tmp2", "/tmp3", "/tmp4"]).unwrap(), - workspace_3 - ); - } - - use crate::workspace_db::model::SerializedWorkspace; - use crate::workspace_db::model::{SerializedItem, SerializedPane, SerializedPaneGroup}; - - fn default_workspace( - dock_pane: SerializedPane, - center_group: &SerializedPaneGroup, - ) -> SerializedWorkspace { - SerializedWorkspace { - dock_anchor: crate::workspace_db::model::DockAnchor::Right, - dock_visible: false, - center_group: center_group.clone(), - dock_pane, - } - } - - #[test] - fn test_basic_dock_pane() { - // env_logger::try_init().ok(); - - let db = Db::open_in_memory("basic_dock_pane"); - - let dock_pane = crate::workspace_db::model::SerializedPane { - children: vec![ - SerializedItem::Terminal { item_id: 1 }, - SerializedItem::Terminal { item_id: 4 }, - SerializedItem::Terminal { item_id: 2 }, - SerializedItem::Terminal { item_id: 3 }, - ], - }; - - let workspace = default_workspace(dock_pane, &Default::default()); - - Workspace::save_workspace(&db, &["/tmp"], None, &workspace); - - let new_workspace = Workspace::workspace_for_roots(&db, &["/tmp"]).unwrap(); - - assert_eq!(workspace.dock_pane, new_workspace.dock_pane); - } - - #[test] - fn test_simple_split() { - // env_logger::try_init().ok(); - - let db = Db::open_in_memory("simple_split"); - - // ----------------- - // | 1,2 | 5,6 | - // | - - - | | - // | 3,4 | | - // ----------------- - let center_pane = SerializedPaneGroup::Group { - axis: crate::workspace_db::model::Axis::Horizontal, - children: vec![ - SerializedPaneGroup::Group { - axis: crate::workspace_db::model::Axis::Vertical, - children: vec![ - SerializedPaneGroup::Pane(SerializedPane { - children: vec![ - SerializedItem::Terminal { item_id: 1 }, - SerializedItem::Terminal { item_id: 2 }, - ], - }), - SerializedPaneGroup::Pane(SerializedPane { - children: vec![ - SerializedItem::Terminal { item_id: 4 }, - SerializedItem::Terminal { item_id: 3 }, - ], - }), - ], - }, - SerializedPaneGroup::Pane(SerializedPane { - children: vec![ - SerializedItem::Terminal { item_id: 5 }, - SerializedItem::Terminal { item_id: 6 }, - ], - }), - ], - }; - - let workspace = default_workspace(Default::default(), ¢er_pane); - - Workspace::save_workspace(&db, &["/tmp"], None, &workspace); - - assert_eq!(workspace.center_group, center_pane); - } -} - -pub mod model { - use std::{ - path::{Path, PathBuf}, - sync::Arc, - }; - - use anyhow::{bail, Result}; - - use sqlez::{ - bindable::{Bind, Column}, - statement::Statement, - }; - - #[derive(Debug, Clone, PartialEq, Eq)] - pub(crate) struct WorkspaceId(Vec); - - impl WorkspaceId { - pub fn paths(self) -> Vec { - self.0 - } - } - - impl, T: IntoIterator> From for WorkspaceId { - fn from(iterator: T) -> Self { - let mut roots = iterator - .into_iter() - .map(|p| p.as_ref().to_path_buf()) - .collect::>(); - roots.sort(); - Self(roots) - } - } - - impl Bind for &WorkspaceId { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - bincode::serialize(&self.0) - .expect("Bincode serialization of paths should not fail") - .bind(statement, start_index) - } - } - - impl Column for WorkspaceId { - 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)) - } - } - - #[derive(Default, Debug, PartialEq, Eq, Clone, Copy)] - pub enum DockAnchor { - #[default] - Bottom, - Right, - Expanded, - } - - impl Bind for DockAnchor { - fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result { - match self { - DockAnchor::Bottom => "Bottom", - DockAnchor::Right => "Right", - DockAnchor::Expanded => "Expanded", - } - .bind(statement, start_index) - } - } - - impl Column for DockAnchor { - fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> { - String::column(statement, start_index).and_then(|(anchor_text, next_index)| { - Ok(( - match anchor_text.as_ref() { - "Bottom" => DockAnchor::Bottom, - "Right" => DockAnchor::Right, - "Expanded" => DockAnchor::Expanded, - _ => bail!("Stored dock anchor is incorrect"), - }, - next_index, - )) - }) - } - } - - #[derive(Debug, PartialEq, Eq)] - pub struct SerializedWorkspace { - pub dock_anchor: DockAnchor, - pub dock_visible: bool, - pub center_group: SerializedPaneGroup, - pub dock_pane: SerializedPane, - } - - #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] - pub enum Axis { - #[default] - Horizontal, - Vertical, - } - - impl Bind for Axis { - fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result { - match self { - Axis::Horizontal => "Horizontal", - Axis::Vertical => "Vertical", - } - .bind(statement, start_index) - } - } - - impl Column for Axis { - fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> { - String::column(statement, start_index).and_then(|(axis_text, next_index)| { - Ok(( - match axis_text.as_str() { - "Horizontal" => Axis::Horizontal, - "Vertical" => Axis::Vertical, - _ => bail!("Stored serialized item kind is incorrect"), - }, - next_index, - )) - }) - } - } - - #[derive(Debug, PartialEq, Eq, Clone)] - pub enum SerializedPaneGroup { - Group { - axis: Axis, - children: Vec, - }, - Pane(SerializedPane), - } - - // Dock panes, and grouped panes combined? - // AND we're collapsing PaneGroup::Pane - // In the case where - - impl Default for SerializedPaneGroup { - fn default() -> Self { - Self::Group { - axis: Axis::Horizontal, - children: vec![Self::Pane(Default::default())], - } - } - } - - #[derive(Debug, PartialEq, Eq, Default, Clone)] - pub struct SerializedPane { - pub(crate) children: Vec, - } - - impl SerializedPane { - pub fn new(children: Vec) -> Self { - SerializedPane { children } - } - } - - pub type GroupId = i64; - pub type PaneId = i64; - pub type ItemId = usize; - - pub(crate) enum SerializedItemKind { - Editor, - Diagnostics, - ProjectSearch, - Terminal, - } - - impl Bind for SerializedItemKind { - fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result { - match self { - SerializedItemKind::Editor => "Editor", - SerializedItemKind::Diagnostics => "Diagnostics", - SerializedItemKind::ProjectSearch => "ProjectSearch", - SerializedItemKind::Terminal => "Terminal", - } - .bind(statement, start_index) - } - } - - impl Column for SerializedItemKind { - fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> { - String::column(statement, start_index).and_then(|(kind_text, next_index)| { - Ok(( - match kind_text.as_ref() { - "Editor" => SerializedItemKind::Editor, - "Diagnostics" => SerializedItemKind::Diagnostics, - "ProjectSearch" => SerializedItemKind::ProjectSearch, - "Terminal" => SerializedItemKind::Terminal, - _ => bail!("Stored serialized item kind is incorrect"), - }, - next_index, - )) - }) - } - } - - #[derive(Debug, PartialEq, Eq, Clone)] - pub enum SerializedItem { - Editor { item_id: usize, path: Arc }, - Diagnostics { item_id: usize }, - ProjectSearch { item_id: usize, query: String }, - Terminal { item_id: usize }, - } - - impl SerializedItem { - pub fn item_id(&self) -> usize { - match self { - SerializedItem::Editor { item_id, .. } => *item_id, - SerializedItem::Diagnostics { item_id } => *item_id, - SerializedItem::ProjectSearch { item_id, .. } => *item_id, - SerializedItem::Terminal { item_id } => *item_id, - } - } - - pub(crate) fn kind(&self) -> SerializedItemKind { - match self { - SerializedItem::Editor { .. } => SerializedItemKind::Editor, - SerializedItem::Diagnostics { .. } => SerializedItemKind::Diagnostics, - SerializedItem::ProjectSearch { .. } => SerializedItemKind::ProjectSearch, - SerializedItem::Terminal { .. } => SerializedItemKind::Terminal, - } - } - } - - #[cfg(test)] - mod tests { - use sqlez::connection::Connection; - - use crate::workspace_db::model::DockAnchor; - - use super::WorkspaceId; - - #[test] - fn test_workspace_round_trips() { - let db = Connection::open_memory("workspace_id_round_trips"); - - db.exec(indoc::indoc! {" - CREATE TABLE workspace_id_test( - workspace_id BLOB, - dock_anchor TEXT - );"}) - .unwrap()() - .unwrap(); - - let workspace_id: WorkspaceId = WorkspaceId::from(&["\test2", "\test1"]); - - db.exec_bound("INSERT INTO workspace_id_test(workspace_id, dock_anchor) VALUES (?,?)") - .unwrap()((&workspace_id, DockAnchor::Bottom)) - .unwrap(); - - assert_eq!( - db.select_row("SELECT workspace_id, dock_anchor FROM workspace_id_test LIMIT 1") - .unwrap()() - .unwrap(), - Some((WorkspaceId::from(&["\test1", "\test2"]), DockAnchor::Bottom)) - ); - } - } -} diff --git a/crates/workspace/test.db b/crates/workspace/test.db new file mode 100644 index 0000000000..7491ccde3a Binary files /dev/null and b/crates/workspace/test.db differ diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 6e7aaba3c6..5f67e290b5 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -23,7 +23,7 @@ use isahc::{config::Configurable, Request}; use language::LanguageRegistry; use log::LevelFilter; use parking_lot::Mutex; -use project::{Db, Fs, HomeDir, ProjectStore}; +use project::{Fs, HomeDir, ProjectStore}; use serde_json::json; use settings::{ self, settings_file::SettingsFile, KeymapFileContent, Settings, SettingsFileContent, @@ -37,12 +37,9 @@ use terminal::terminal_container_view::{get_working_directory, TerminalContainer use fs::RealFs; use settings::watched_json::{watch_keymap_file, watch_settings_file, WatchedJsonFile}; use theme::ThemeRegistry; -use util::{paths, ResultExt, TryFutureExt}; +use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt}; use workspace::{self, AppState, ItemHandle, NewFile, OpenPaths, Workspace}; -use zed::{ - self, build_window_options, initialize_workspace, languages, menus, RELEASE_CHANNEL, - RELEASE_CHANNEL_NAME, -}; +use zed::{self, build_window_options, initialize_workspace, languages, menus}; fn main() { let http = http::client(); @@ -56,10 +53,6 @@ fn main() { .map_or("dev".to_string(), |v| v.to_string()); init_panic_hook(app_version, http.clone(), app.background()); - let db = app.background().spawn(async move { - project::Db::::open(&*paths::DB_DIR, RELEASE_CHANNEL_NAME.as_str()) - }); - load_embedded_fonts(&app); let fs = Arc::new(RealFs); @@ -147,10 +140,8 @@ fn main() { .detach(); let project_store = cx.add_model(|_| ProjectStore::new()); - let db = cx.background().block(db); - cx.set_global(db); - client.start_telemetry(cx.global::>().clone()); + client.start_telemetry(); client.report_event("start app", Default::default()); let app_state = Arc::new(AppState { @@ -164,16 +155,9 @@ fn main() { initialize_workspace, default_item_factory, }); - auto_update::init( - cx.global::>().clone(), - http, - client::ZED_SERVER_URL.clone(), - cx, - ); + auto_update::init(http, client::ZED_SERVER_URL.clone(), cx); - let workspace_db = cx.global::>().open_as::(); - - workspace::init(app_state.clone(), cx, workspace_db); + workspace::init(app_state.clone(), cx); journal::init(app_state.clone(), cx); theme_selector::init(app_state.clone(), cx); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index a8ec71bd4b..6b6b65ab32 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -12,7 +12,6 @@ use collab_ui::{CollabTitlebarItem, ToggleCollaborationMenu}; use collections::VecDeque; pub use editor; use editor::{Editor, MultiBuffer}; -use lazy_static::lazy_static; use gpui::{ actions, @@ -28,9 +27,9 @@ use project_panel::ProjectPanel; use search::{BufferSearchBar, ProjectSearchBar}; use serde::Deserialize; use serde_json::to_string_pretty; -use settings::{keymap_file_json_schema, settings_file_json_schema, ReleaseChannel, Settings}; +use settings::{keymap_file_json_schema, settings_file_json_schema, Settings}; use std::{env, path::Path, str, sync::Arc}; -use util::{paths, ResultExt}; +use util::{channel::ReleaseChannel, paths, ResultExt}; pub use workspace; use workspace::{sidebar::SidebarSide, AppState, Workspace}; @@ -69,17 +68,6 @@ actions!( const MIN_FONT_SIZE: f32 = 6.0; -lazy_static! { - pub static ref RELEASE_CHANNEL_NAME: String = - env::var("ZED_RELEASE_CHANNEL").unwrap_or(include_str!("../RELEASE_CHANNEL").to_string()); - pub static ref RELEASE_CHANNEL: ReleaseChannel = match RELEASE_CHANNEL_NAME.as_str() { - "dev" => ReleaseChannel::Dev, - "preview" => ReleaseChannel::Preview, - "stable" => ReleaseChannel::Stable, - _ => panic!("invalid release channel {}", *RELEASE_CHANNEL_NAME), - }; -} - pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { cx.add_action(about); cx.add_global_action(|_: &Hide, cx: &mut gpui::MutableAppContext| { @@ -629,7 +617,7 @@ mod tests { use gpui::{ executor::Deterministic, AssetSource, MutableAppContext, TestAppContext, ViewHandle, }; - use project::{Db, Project, ProjectPath}; + use project::{Project, ProjectPath}; use serde_json::json; use std::{ collections::HashSet, @@ -774,6 +762,8 @@ mod tests { async fn test_new_empty_workspace(cx: &mut TestAppContext) { let app_state = init(cx); cx.dispatch_global_action(workspace::NewFile); + cx.foreground().run_until_parked(); + let window_id = *cx.window_ids().first().unwrap(); let workspace = cx.root_view::(window_id).unwrap(); let editor = workspace.update(cx, |workspace, cx| { @@ -1816,7 +1806,7 @@ mod tests { state.initialize_workspace = initialize_workspace; state.build_window_options = build_window_options; call::init(app_state.client.clone(), app_state.user_store.clone(), cx); - workspace::init(app_state.clone(), cx, Db::open_in_memory("test")); + workspace::init(app_state.clone(), cx); editor::init(cx); pane::init(cx); app_state