mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-03 22:28:35 +00:00
Built first draft of workspace serialization schemas, started writing DB tests
Co-Authored-By: kay@zed.dev
This commit is contained in:
parent
60ebe33518
commit
b48e28b555
5 changed files with 234 additions and 301 deletions
|
@ -1,6 +1,7 @@
|
|||
mod items;
|
||||
mod kvp;
|
||||
mod migrations;
|
||||
mod workspace;
|
||||
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
|
|
@ -6,306 +6,59 @@ use rusqlite::{named_params, params};
|
|||
|
||||
use super::Db;
|
||||
|
||||
/// Current design makes the cut at the item level,
|
||||
/// - Maybe A little more bottom up, serialize 'Terminals' and 'Editors' directly, and then make a seperate
|
||||
/// - items table, with a kind, and an integer that acts as a key to one of these other tables
|
||||
/// This column is a foreign key to ONE OF: editors, terminals, searches
|
||||
/// -
|
||||
|
||||
// (workspace_id, item_id)
|
||||
// kind -> ::Editor::
|
||||
|
||||
// ->
|
||||
// At the workspace level
|
||||
// -> (Workspace_ID, item_id)
|
||||
// -> One shot, big query, load everything up:
|
||||
|
||||
// -> SerializedWorkspace::deserialize(tx, itemKey)
|
||||
// -> SerializedEditor::deserialize(tx, itemKey)
|
||||
|
||||
// ->
|
||||
// -> Workspace::new(SerializedWorkspace)
|
||||
// -> Editor::new(serialized_workspace[???]serializedEditor)
|
||||
|
||||
// //Pros: Keeps sql out of every body elese, makes changing it easier (e.g. for loading from a network or RocksDB)
|
||||
// //Cons: DB has to know the internals of the entire rest of the app
|
||||
|
||||
// Workspace
|
||||
// Worktree roots
|
||||
// Pane groups
|
||||
// Dock
|
||||
// Items
|
||||
// Sidebars
|
||||
|
||||
pub(crate) const ITEMS_M_1: &str = "
|
||||
CREATE TABLE items(
|
||||
id INTEGER PRIMARY KEY,
|
||||
kind TEXT
|
||||
workspace_id INTEGER,
|
||||
item_id INTEGER,
|
||||
kind TEXT NOT NULL,
|
||||
PRIMARY KEY (workspace_id, item_id)
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspace_ids(workspace_id)
|
||||
) STRICT;
|
||||
CREATE TABLE item_path(
|
||||
item_id INTEGER PRIMARY KEY,
|
||||
path BLOB
|
||||
|
||||
CREATE TABLE project_searches(
|
||||
workspace_id INTEGER,
|
||||
item_id INTEGER,
|
||||
query TEXT,
|
||||
PRIMARY KEY (workspace_id, item_id)
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspace_ids(workspace_id)
|
||||
) STRICT;
|
||||
CREATE TABLE item_query(
|
||||
item_id INTEGER PRIMARY KEY,
|
||||
query TEXT
|
||||
|
||||
CREATE TABLE editors(
|
||||
workspace_id INTEGER,
|
||||
item_id INTEGER,
|
||||
path BLOB NOT NULL,
|
||||
PRIMARY KEY (workspace_id, item_id)
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspace_ids(workspace_id)
|
||||
) STRICT;
|
||||
";
|
||||
|
||||
#[derive(PartialEq, Eq, Hash, Debug)]
|
||||
pub enum SerializedItemKind {
|
||||
Editor,
|
||||
Terminal,
|
||||
ProjectSearch,
|
||||
Diagnostics,
|
||||
}
|
||||
|
||||
impl Display for SerializedItemKind {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&format!("{:?}", self))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum SerializedItem {
|
||||
Editor(usize, PathBuf),
|
||||
Terminal(usize),
|
||||
ProjectSearch(usize, String),
|
||||
Diagnostics(usize),
|
||||
}
|
||||
|
||||
impl SerializedItem {
|
||||
fn kind(&self) -> SerializedItemKind {
|
||||
match self {
|
||||
SerializedItem::Editor(_, _) => SerializedItemKind::Editor,
|
||||
SerializedItem::Terminal(_) => SerializedItemKind::Terminal,
|
||||
SerializedItem::ProjectSearch(_, _) => SerializedItemKind::ProjectSearch,
|
||||
SerializedItem::Diagnostics(_) => SerializedItemKind::Diagnostics,
|
||||
}
|
||||
}
|
||||
|
||||
fn id(&self) -> usize {
|
||||
match self {
|
||||
SerializedItem::Editor(id, _)
|
||||
| SerializedItem::Terminal(id)
|
||||
| SerializedItem::ProjectSearch(id, _)
|
||||
| SerializedItem::Diagnostics(id) => *id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Db {
|
||||
fn write_item(&self, serialized_item: SerializedItem) -> Result<()> {
|
||||
self.real()
|
||||
.map(|db| {
|
||||
let mut lock = db.connection.lock();
|
||||
let tx = lock.transaction()?;
|
||||
|
||||
// Serialize the item
|
||||
let id = serialized_item.id();
|
||||
{
|
||||
let mut stmt = tx.prepare_cached(
|
||||
"INSERT OR REPLACE INTO items(id, kind) VALUES ((?), (?))",
|
||||
)?;
|
||||
|
||||
dbg!("inserting item");
|
||||
stmt.execute(params![id, serialized_item.kind().to_string()])?;
|
||||
}
|
||||
|
||||
// Serialize item data
|
||||
match &serialized_item {
|
||||
SerializedItem::Editor(_, path) => {
|
||||
dbg!("inserting path");
|
||||
let mut stmt = tx.prepare_cached(
|
||||
"INSERT OR REPLACE INTO item_path(item_id, path) VALUES ((?), (?))",
|
||||
)?;
|
||||
|
||||
let path_bytes = path.as_os_str().as_bytes();
|
||||
stmt.execute(params![id, path_bytes])?;
|
||||
}
|
||||
SerializedItem::ProjectSearch(_, query) => {
|
||||
dbg!("inserting query");
|
||||
let mut stmt = tx.prepare_cached(
|
||||
"INSERT OR REPLACE INTO item_query(item_id, query) VALUES ((?), (?))",
|
||||
)?;
|
||||
|
||||
stmt.execute(params![id, query])?;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
tx.commit()?;
|
||||
|
||||
let mut stmt = lock.prepare_cached("SELECT id, kind FROM items")?;
|
||||
let _ = stmt
|
||||
.query_map([], |row| {
|
||||
let zero: usize = row.get(0)?;
|
||||
let one: String = row.get(1)?;
|
||||
|
||||
dbg!(zero, one);
|
||||
Ok(())
|
||||
})?
|
||||
.collect::<Vec<Result<(), _>>>();
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.unwrap_or(Ok(()))
|
||||
}
|
||||
|
||||
fn delete_item(&self, item_id: usize) -> Result<()> {
|
||||
self.real()
|
||||
.map(|db| {
|
||||
let lock = db.connection.lock();
|
||||
|
||||
let mut stmt = lock.prepare_cached(
|
||||
r#"
|
||||
DELETE FROM items WHERE id = (:id);
|
||||
DELETE FROM item_path WHERE id = (:id);
|
||||
DELETE FROM item_query WHERE id = (:id);
|
||||
"#,
|
||||
)?;
|
||||
|
||||
stmt.execute(named_params! {":id": item_id})?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.unwrap_or(Ok(()))
|
||||
}
|
||||
|
||||
fn take_items(&self) -> Result<HashSet<SerializedItem>> {
|
||||
self.real()
|
||||
.map(|db| {
|
||||
let mut lock = db.connection.lock();
|
||||
|
||||
let tx = lock.transaction()?;
|
||||
|
||||
// When working with transactions in rusqlite, need to make this kind of scope
|
||||
// To make the borrow stuff work correctly. Don't know why, rust is wild.
|
||||
let result = {
|
||||
let mut editors_stmt = tx.prepare_cached(
|
||||
r#"
|
||||
SELECT items.id, item_path.path
|
||||
FROM items
|
||||
LEFT JOIN item_path
|
||||
ON items.id = item_path.item_id
|
||||
WHERE items.kind = ?;
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let editors_iter = editors_stmt.query_map(
|
||||
[SerializedItemKind::Editor.to_string()],
|
||||
|row| {
|
||||
let id: usize = row.get(0)?;
|
||||
|
||||
let buf: Vec<u8> = row.get(1)?;
|
||||
let path: PathBuf = OsStr::from_bytes(&buf).into();
|
||||
|
||||
Ok(SerializedItem::Editor(id, path))
|
||||
},
|
||||
)?;
|
||||
|
||||
let mut terminals_stmt = tx.prepare_cached(
|
||||
r#"
|
||||
SELECT items.id
|
||||
FROM items
|
||||
WHERE items.kind = ?;
|
||||
"#,
|
||||
)?;
|
||||
let terminals_iter = terminals_stmt.query_map(
|
||||
[SerializedItemKind::Terminal.to_string()],
|
||||
|row| {
|
||||
let id: usize = row.get(0)?;
|
||||
|
||||
Ok(SerializedItem::Terminal(id))
|
||||
},
|
||||
)?;
|
||||
|
||||
let mut search_stmt = tx.prepare_cached(
|
||||
r#"
|
||||
SELECT items.id, item_query.query
|
||||
FROM items
|
||||
LEFT JOIN item_query
|
||||
ON items.id = item_query.item_id
|
||||
WHERE items.kind = ?;
|
||||
"#,
|
||||
)?;
|
||||
let searches_iter = search_stmt.query_map(
|
||||
[SerializedItemKind::ProjectSearch.to_string()],
|
||||
|row| {
|
||||
let id: usize = row.get(0)?;
|
||||
let query = row.get(1)?;
|
||||
|
||||
Ok(SerializedItem::ProjectSearch(id, query))
|
||||
},
|
||||
)?;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
let tmp =
|
||||
searches_iter.collect::<Vec<Result<SerializedItem, rusqlite::Error>>>();
|
||||
#[cfg(debug_assertions)]
|
||||
debug_assert!(tmp.len() == 0 || tmp.len() == 1);
|
||||
#[cfg(debug_assertions)]
|
||||
let searches_iter = tmp.into_iter();
|
||||
|
||||
let mut diagnostic_stmt = tx.prepare_cached(
|
||||
r#"
|
||||
SELECT items.id
|
||||
FROM items
|
||||
WHERE items.kind = ?;
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let diagnostics_iter = diagnostic_stmt.query_map(
|
||||
[SerializedItemKind::Diagnostics.to_string()],
|
||||
|row| {
|
||||
let id: usize = row.get(0)?;
|
||||
|
||||
Ok(SerializedItem::Diagnostics(id))
|
||||
},
|
||||
)?;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
let tmp =
|
||||
diagnostics_iter.collect::<Vec<Result<SerializedItem, rusqlite::Error>>>();
|
||||
#[cfg(debug_assertions)]
|
||||
debug_assert!(tmp.len() == 0 || tmp.len() == 1);
|
||||
#[cfg(debug_assertions)]
|
||||
let diagnostics_iter = tmp.into_iter();
|
||||
|
||||
let res = editors_iter
|
||||
.chain(terminals_iter)
|
||||
.chain(diagnostics_iter)
|
||||
.chain(searches_iter)
|
||||
.collect::<Result<HashSet<SerializedItem>, rusqlite::Error>>()?;
|
||||
|
||||
let mut delete_stmt = tx.prepare_cached(
|
||||
r#"
|
||||
DELETE FROM items;
|
||||
DELETE FROM item_path;
|
||||
DELETE FROM item_query;
|
||||
"#,
|
||||
)?;
|
||||
|
||||
delete_stmt.execute([])?;
|
||||
|
||||
res
|
||||
};
|
||||
|
||||
tx.commit()?;
|
||||
|
||||
Ok(result)
|
||||
})
|
||||
.unwrap_or(Ok(HashSet::default()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use anyhow::Result;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_items_round_trip() -> Result<()> {
|
||||
let db = Db::open_in_memory();
|
||||
|
||||
let mut items = vec![
|
||||
SerializedItem::Editor(0, PathBuf::from("/tmp/test.txt")),
|
||||
SerializedItem::Terminal(1),
|
||||
SerializedItem::ProjectSearch(2, "Test query!".to_string()),
|
||||
SerializedItem::Diagnostics(3),
|
||||
]
|
||||
.into_iter()
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
for item in items.iter() {
|
||||
dbg!("Inserting... ");
|
||||
db.write_item(item.clone())?;
|
||||
}
|
||||
|
||||
assert_eq!(items, db.take_items()?);
|
||||
|
||||
// Check that it's empty, as expected
|
||||
assert_eq!(HashSet::default(), db.take_items()?);
|
||||
|
||||
for item in items.iter() {
|
||||
db.write_item(item.clone())?;
|
||||
}
|
||||
|
||||
items.remove(&SerializedItem::ProjectSearch(2, "Test query!".to_string()));
|
||||
db.delete_item(2)?;
|
||||
|
||||
assert_eq!(items, db.take_items()?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ use rusqlite::OptionalExtension;
|
|||
|
||||
use super::Db;
|
||||
|
||||
pub(crate) const KVP_M_1_UP: &str = "
|
||||
pub(crate) const KVP_M_1: &str = "
|
||||
CREATE TABLE kv_store(
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use rusqlite_migration::{Migrations, M};
|
||||
|
||||
// use crate::items::ITEMS_M_1;
|
||||
use crate::{items::ITEMS_M_1, kvp::KVP_M_1_UP};
|
||||
use crate::kvp::KVP_M_1;
|
||||
|
||||
// This must be ordered by development time! Only ever add new migrations to the end!!
|
||||
// Bad things will probably happen if you don't monotonically edit this vec!!!!
|
||||
|
@ -9,7 +9,6 @@ use crate::{items::ITEMS_M_1, kvp::KVP_M_1_UP};
|
|||
// file system and so everything we do here is locked in _f_o_r_e_v_e_r_.
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref MIGRATIONS: Migrations<'static> = Migrations::new(vec![
|
||||
M::up(KVP_M_1_UP),
|
||||
M::up(ITEMS_M_1),
|
||||
M::up(KVP_M_1),
|
||||
]);
|
||||
}
|
||||
|
|
180
crates/db/src/workspace.rs
Normal file
180
crates/db/src/workspace.rs
Normal file
|
@ -0,0 +1,180 @@
|
|||
use std::{path::Path, sync::Arc};
|
||||
|
||||
use super::Db;
|
||||
|
||||
pub(crate) const WORKSPACE_M_1: &str = "
|
||||
CREATE TABLE workspaces(
|
||||
workspace_id INTEGER PRIMARY KEY,
|
||||
center_group INTEGER NOT NULL,
|
||||
dock_pane INTEGER NOT NULL,
|
||||
timestamp INTEGER,
|
||||
FOREIGN KEY(center_group) REFERENCES pane_groups(group_id)
|
||||
FOREIGN KEY(dock_pane) REFERENCES pane_items(pane_id)
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE worktree_roots(
|
||||
worktree_root BLOB NOT NULL,
|
||||
workspace_id INTEGER NOT NULL,
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspace_ids(workspace_id)
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE pane_groups(
|
||||
workspace_id INTEGER,
|
||||
group_id INTEGER,
|
||||
split_direction STRING, -- 'Vertical' / 'Horizontal' /
|
||||
PRIMARY KEY (workspace_id, group_id)
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE pane_group_children(
|
||||
workspace_id INTEGER,
|
||||
group_id INTEGER,
|
||||
child_pane_id INTEGER, -- Nullable
|
||||
child_group_id INTEGER, -- Nullable
|
||||
PRIMARY KEY (workspace_id, group_id)
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE pane_items(
|
||||
workspace_id INTEGER,
|
||||
pane_id INTEGER,
|
||||
item_id INTEGER, -- Array
|
||||
PRIMARY KEY (workspace_id, pane_id)
|
||||
) STRICT;
|
||||
";
|
||||
|
||||
// Zed stores items with ids which are a combination of a view id during a given run and a workspace id. This
|
||||
|
||||
// Case 1: Starting Zed Contextless
|
||||
// > Zed -> Reopen the last
|
||||
// Case 2: Starting Zed with a project folder
|
||||
// > Zed ~/projects/Zed
|
||||
// Case 3: Starting Zed with a file
|
||||
// > Zed ~/projects/Zed/cargo.toml
|
||||
// Case 4: Starting Zed with multiple project folders
|
||||
// > Zed ~/projects/Zed ~/projects/Zed.dev
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct WorkspaceId(usize);
|
||||
|
||||
impl Db {
|
||||
/// Finds or creates a workspace id for the given set of worktree roots. If the passed worktree roots is empty, return the
|
||||
/// the last workspace id
|
||||
pub fn workspace_id(&self, worktree_roots: &[Arc<Path>]) -> WorkspaceId {
|
||||
// Find the workspace id which is uniquely identified by this set of paths return it if found
|
||||
// Otherwise:
|
||||
// Find the max workspace_id and increment it as our new workspace id
|
||||
// Store in the worktrees table the mapping from this new id to the set of worktree roots
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
/// Updates the open paths for the given workspace id. Will garbage collect items from
|
||||
/// any workspace ids which are no replaced by the new workspace id. Updates the timestamps
|
||||
/// in the workspace id table
|
||||
pub fn update_worktree_roots(&self, workspace_id: &WorkspaceId, worktree_roots: &[Arc<Path>]) {
|
||||
// Lookup any WorkspaceIds which have the same set of roots, and delete them. (NOTE: this should garbage collect other tables)
|
||||
// Remove the old rows which contain workspace_id
|
||||
// Add rows for the new worktree_roots
|
||||
|
||||
// zed /tree
|
||||
// -> add tree2
|
||||
// -> udpate_worktree_roots() -> ADDs entries for /tree and /tree2, LEAVING BEHIND, the initial entry for /tree
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
/// Returns the previous workspace ids sorted by last modified
|
||||
pub fn recent_workspaces(&self) -> Vec<(WorkspaceId, Vec<Arc<Path>>)> {
|
||||
// Return all the workspace ids and their associated paths ordered by the access timestamp
|
||||
//ORDER BY timestamps
|
||||
unimplemented!();
|
||||
}
|
||||
|
||||
pub fn center_pane(&self, workspace: WorkspaceId) -> SerializedPaneGroup {}
|
||||
|
||||
pub fn dock_pane(&self, workspace: WorkspaceId) -> SerializedPane {}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use crate::Db;
|
||||
|
||||
use super::WorkspaceId;
|
||||
|
||||
fn test_tricky_overlapping_updates() {
|
||||
// DB state:
|
||||
// (/tree) -> ID: 1
|
||||
// (/tree, /tree2) -> ID: 2
|
||||
// (/tree2, /tree3) -> ID: 3
|
||||
|
||||
// -> User updates 2 to: (/tree2, /tree3)
|
||||
|
||||
// DB state:
|
||||
// (/tree) -> ID: 1
|
||||
// (/tree2, /tree3) -> ID: 2
|
||||
// Get rid of 3 for garbage collection
|
||||
|
||||
fn arc_path(path: &'static str) -> Arc<Path> {
|
||||
PathBuf::from(path).into()
|
||||
}
|
||||
|
||||
let data = &[
|
||||
(WorkspaceId(1), vec![arc_path("/tmp")]),
|
||||
(WorkspaceId(2), vec![arc_path("/tmp"), arc_path("/tmp2")]),
|
||||
(WorkspaceId(3), vec![arc_path("/tmp2"), arc_path("/tmp3")]),
|
||||
];
|
||||
|
||||
let db = Db::open_in_memory();
|
||||
|
||||
for (workspace_id, entries) in data {
|
||||
db.update_worktree_roots(workspace_id, entries); //??
|
||||
assert_eq!(&db.workspace_id(&[]), workspace_id)
|
||||
}
|
||||
|
||||
for (workspace_id, entries) in data {
|
||||
assert_eq!(&db.workspace_id(entries.as_slice()), workspace_id);
|
||||
}
|
||||
|
||||
db.update_worktree_roots(&WorkspaceId(2), &[arc_path("/tmp2")]);
|
||||
// todo!(); // make sure that 3 got garbage collected
|
||||
|
||||
assert_eq!(db.workspace_id(&[arc_path("/tmp2")]), WorkspaceId(2));
|
||||
assert_eq!(db.workspace_id(&[arc_path("/tmp")]), WorkspaceId(1));
|
||||
|
||||
let recent_workspaces = db.recent_workspaces();
|
||||
assert_eq!(recent_workspaces.get(0).unwrap().0, WorkspaceId(2));
|
||||
assert_eq!(recent_workspaces.get(1).unwrap().0, WorkspaceId(3));
|
||||
assert_eq!(recent_workspaces.get(2).unwrap().0, WorkspaceId(1));
|
||||
}
|
||||
}
|
||||
|
||||
// [/tmp, /tmp2] -> ID1?
|
||||
// [/tmp] -> ID2?
|
||||
|
||||
/*
|
||||
path | id
|
||||
/tmp ID1
|
||||
/tmp ID2
|
||||
/tmp2 ID1
|
||||
|
||||
|
||||
SELECT id
|
||||
FROM workspace_ids
|
||||
WHERE path IN (path1, path2)
|
||||
INTERSECT
|
||||
SELECT id
|
||||
FROM workspace_ids
|
||||
WHERE path = path_2
|
||||
... and etc. for each element in path array
|
||||
|
||||
If contains row, yay! If not,
|
||||
SELECT max(id) FROm workspace_ids
|
||||
|
||||
Select id WHERE path IN paths
|
||||
|
||||
SELECT MAX(id)
|
||||
|
||||
*/
|
Loading…
Reference in a new issue