Finished implementing the workspace stuff

This commit is contained in:
Mikayla Maki
2022-12-03 16:05:25 -08:00
parent 395070cb92
commit 777f05eb76
10 changed files with 263 additions and 278 deletions
+5 -2
View File
@@ -10,6 +10,8 @@ use std::path::Path;
use anyhow::Result;
use indoc::indoc;
use kvp::KVP_MIGRATION;
use pane::PANE_MIGRATIONS;
use sqlez::connection::Connection;
use sqlez::thread_safe_connection::ThreadSafeConnection;
@@ -42,7 +44,8 @@ impl Db {
PRAGMA synchronous=NORMAL;
PRAGMA foreign_keys=TRUE;
PRAGMA case_sensitive_like=TRUE;
"}),
"})
.with_migrations(&[KVP_MIGRATION, WORKSPACES_MIGRATION, PANE_MIGRATIONS]),
)
}
@@ -64,7 +67,7 @@ impl Db {
pub fn write_to<P: AsRef<Path>>(&self, dest: P) -> Result<()> {
let destination = Connection::open_file(dest.as_ref().to_string_lossy().as_ref());
self.backup(&destination)
self.backup_main(&destination)
}
}
+24 -41
View File
@@ -1,55 +1,38 @@
use anyhow::Result;
use rusqlite::OptionalExtension;
use super::Db;
use anyhow::Result;
use indoc::indoc;
use sqlez::migrations::Migration;
pub(crate) const KVP_M_1: &str = "
CREATE TABLE kv_store(
key TEXT PRIMARY KEY,
value TEXT NOT NULL
) STRICT;
";
pub(crate) const KVP_MIGRATION: Migration = Migration::new(
"kvp",
&[indoc! {"
CREATE TABLE kv_store(
key TEXT PRIMARY KEY,
value TEXT NOT NULL
) STRICT;
"}],
);
impl Db {
pub fn read_kvp(&self, key: &str) -> Result<Option<String>> {
self.real()
.map(|db| {
let lock = db.connection.lock();
let mut stmt = lock.prepare_cached("SELECT value FROM kv_store WHERE key = (?)")?;
Ok(stmt.query_row([key], |row| row.get(0)).optional()?)
})
.unwrap_or(Ok(None))
self.0
.prepare("SELECT value FROM kv_store WHERE key = (?)")?
.bind(key)?
.maybe_row()
}
pub fn write_kvp(&self, key: &str, value: &str) -> Result<()> {
self.real()
.map(|db| {
let lock = db.connection.lock();
let mut stmt = lock.prepare_cached(
"INSERT OR REPLACE INTO kv_store(key, value) VALUES ((?), (?))",
)?;
stmt.execute([key, value])?;
Ok(())
})
.unwrap_or(Ok(()))
self.0
.prepare("INSERT OR REPLACE INTO kv_store(key, value) VALUES (?, ?)")?
.bind((key, value))?
.exec()
}
pub fn delete_kvp(&self, key: &str) -> Result<()> {
self.real()
.map(|db| {
let lock = db.connection.lock();
let mut stmt = lock.prepare_cached("DELETE FROM kv_store WHERE key = (?)")?;
stmt.execute([key])?;
Ok(())
})
.unwrap_or(Ok(()))
self.0
.prepare("DELETE FROM kv_store WHERE key = (?)")?
.bind(key)?
.exec()
}
}
+13 -15
View File
@@ -1,16 +1,14 @@
use rusqlite_migration::{Migrations, M};
// // use crate::items::ITEMS_M_1;
// use crate::{kvp::KVP_M_1, pane::PANE_M_1, WORKSPACES_MIGRATION};
// use crate::items::ITEMS_M_1;
use crate::{kvp::KVP_M_1, pane::PANE_M_1, WORKSPACE_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!!!!
// And no re-ordering ever!!!!!!!!!! The results of these migrations are on the user's
// 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),
M::up(WORKSPACE_M_1),
M::up(PANE_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!!!!
// // And no re-ordering ever!!!!!!!!!! The results of these migrations are on the user's
// // 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),
// M::up(WORKSPACE_M_1),
// M::up(PANE_M_1)
// ]);
// }
+8 -10
View File
@@ -1,15 +1,14 @@
use gpui::Axis;
use indoc::indoc;
use sqlez::migrations::Migration;
use rusqlite::{OptionalExtension, Connection};
use serde::{Deserialize, Serialize};
use serde_rusqlite::{from_row, to_params_named};
use crate::{items::ItemId, workspace::WorkspaceId};
use super::Db;
pub(crate) const PANE_M_1: &str = "
pub(crate) const PANE_MIGRATIONS: Migration = Migration::new("pane", &[indoc! {"
CREATE TABLE dock_panes(
dock_pane_id INTEGER PRIMARY KEY,
workspace_id INTEGER NOT NULL,
@@ -64,7 +63,7 @@ CREATE TABLE dock_items(
FOREIGN KEY(dock_pane_id) REFERENCES dock_panes(dock_pane_id) ON DELETE CASCADE,
FOREIGN KEY(item_id) REFERENCES items(item_id)ON DELETE CASCADE
) STRICT;
";
"}]);
// We have an many-branched, unbalanced tree with three types:
// Pane Groups
@@ -140,7 +139,7 @@ pub struct SerializedPane {
//********* CURRENTLY IN USE TYPES: *********
#[derive(Default, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[derive(Default, Debug, PartialEq, Eq)]
pub enum DockAnchor {
#[default]
Bottom,
@@ -148,7 +147,7 @@ pub enum DockAnchor {
Expanded,
}
#[derive(Default, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[derive(Default, Debug, PartialEq, Eq)]
pub struct SerializedDockPane {
pub anchor_position: DockAnchor,
pub visible: bool,
@@ -160,7 +159,7 @@ impl SerializedDockPane {
}
}
#[derive(Default, Debug, PartialEq, Eq, Deserialize, Serialize)]
#[derive(Default, Debug, PartialEq, Eq)]
pub(crate) struct DockRow {
workspace_id: WorkspaceId,
anchor_position: DockAnchor,
@@ -298,12 +297,11 @@ mod tests {
let workspace = db.workspace_for_roots(&["/tmp"]);
let dock_pane = SerializedDockPane {
workspace_id: workspace.workspace_id,
anchor_position: DockAnchor::Expanded,
visible: true,
};
db.save_dock_pane(&dock_pane);
db.save_dock_pane(workspace.workspace_id, dock_pane);
let new_workspace = db.workspace_for_roots(&["/tmp"]);
+96 -185
View File
@@ -6,12 +6,12 @@ use std::{
os::unix::prelude::OsStrExt,
path::{Path, PathBuf},
sync::Arc,
time::{SystemTime, UNIX_EPOCH},
};
use anyhow::Result;
use indoc::indoc;
use sqlez::{connection::Connection, migrations::Migration};
use sqlez::{
connection::Connection, migrations::Migration,
};
use crate::pane::SerializedDockPane;
@@ -20,8 +20,8 @@ use super::Db;
// If you need to debug the worktree root code, change 'BLOB' here to 'TEXT' for easier debugging
// you might want to update some of the parsing code as well, I've left the variations in but commented
// out. This will panic if run on an existing db that has already been migrated
const WORKSPACES_MIGRATION: Migration = Migration::new(
"migrations",
pub(crate) const WORKSPACES_MIGRATION: Migration = Migration::new(
"workspace",
&[indoc! {"
CREATE TABLE workspaces(
workspace_id INTEGER PRIMARY KEY,
@@ -53,8 +53,8 @@ pub struct SerializedWorkspace {
}
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
/// Finds or creates a workspace id for the given set of worktree roots. If the passed worktree roots is empty,
/// returns the last workspace which was updated
pub fn workspace_for_roots<P>(&self, worktree_roots: &[P]) -> SerializedWorkspace
where
P: AsRef<Path> + Debug,
@@ -80,23 +80,21 @@ impl Db {
where
P: AsRef<Path> + Debug,
{
let result = (|| {
let tx = self.transaction()?;
tx.execute("INSERT INTO workspaces(last_opened_timestamp) VALUES" (?), [current_millis()?])?;
let res = self.with_savepoint("make_new_workspace", |conn| {
let workspace_id = WorkspaceId(
conn.prepare("INSERT INTO workspaces DEFAULT VALUES")?
.insert()?,
);
let id = WorkspaceId(tx.last_insert_rowid());
update_worktree_roots(&tx, &id, worktree_roots)?;
tx.commit()?;
update_worktree_roots(conn, &workspace_id, worktree_roots)?;
Ok(SerializedWorkspace {
workspace_id: id,
workspace_id,
dock_pane: None,
})
})();
});
match result {
match res {
Ok(serialized_workspace) => serialized_workspace,
Err(err) => {
log::error!("Failed to insert new workspace into DB: {}", err);
@@ -109,19 +107,13 @@ impl Db {
where
P: AsRef<Path> + Debug,
{
self.real()
.map(|db| {
let lock = db.connection.lock();
match get_workspace_id(worktree_roots, &lock) {
Ok(workspace_id) => workspace_id,
Err(err) => {
log::error!("Failed to get workspace_id: {}", err);
None
}
}
})
.unwrap_or(None)
match get_workspace_id(worktree_roots, &self) {
Ok(workspace_id) => workspace_id,
Err(err) => {
log::error!("Failed to get workspace_id: {}", err);
None
}
}
}
// fn get_workspace_row(&self, workspace_id: WorkspaceId) -> WorkspaceRow {
@@ -135,123 +127,73 @@ impl Db {
where
P: AsRef<Path> + Debug,
{
fn logic<P>(
connection: &mut Connection,
workspace_id: &WorkspaceId,
worktree_roots: &[P],
) -> Result<()>
where
P: AsRef<Path> + Debug,
{
let tx = connection.transaction()?;
update_worktree_roots(&tx, workspace_id, worktree_roots)?;
tx.commit()?;
Ok(())
match self.with_savepoint("update_worktrees", |conn| {
update_worktree_roots(conn, workspace_id, worktree_roots)
}) {
Ok(_) => {}
Err(err) => log::error!(
"Failed to update workspace {:?} with roots {:?}, error: {}",
workspace_id,
worktree_roots,
err
),
}
self.real().map(|db| {
let mut lock = db.connection.lock();
match logic(&mut lock, workspace_id, worktree_roots) {
Ok(_) => {}
Err(err) => {
log::error!(
"Failed to update the worktree roots for {:?}, roots: {:?}, error: {}",
workspace_id,
worktree_roots,
err
);
}
}
});
}
fn last_workspace_id(&self) -> Option<WorkspaceId> {
fn logic(connection: &mut Connection) -> Result<Option<WorkspaceId>> {
let mut stmt = connection.prepare(
let res = self
.prepare(
"SELECT workspace_id FROM workspaces ORDER BY last_opened_timestamp DESC LIMIT 1",
)?;
)
.and_then(|stmt| stmt.maybe_row())
.map(|row| row.map(|id| WorkspaceId(id)));
Ok(stmt
.query_row([], |row| Ok(WorkspaceId(row.get(0)?)))
.optional()?)
match res {
Ok(result) => result,
Err(err) => {
log::error!("Failed to get last workspace id, err: {}", err);
return None;
}
}
self.real()
.map(|db| {
let mut lock = db.connection.lock();
match logic(&mut lock) {
Ok(result) => result,
Err(err) => {
log::error!("Failed to get last workspace id, err: {}", err);
None
}
}
})
.unwrap_or(None)
}
/// Returns the previous workspace ids sorted by last modified along with their opened worktree roots
pub fn recent_workspaces(&self, limit: usize) -> Vec<(WorkspaceId, Vec<Arc<Path>>)> {
fn logic(
connection: &mut Connection,
limit: usize,
) -> Result<Vec<(WorkspaceId, Vec<Arc<Path>>)>, anyhow::Error> {
let tx = connection.transaction()?;
let result = {
let mut stmt = tx.prepare(
"SELECT workspace_id FROM workspaces ORDER BY last_opened_timestamp DESC LIMIT ?",
)?;
let workspace_ids = stmt
.query_map([limit], |row| Ok(WorkspaceId(row.get(0)?)))?
.collect::<Result<Vec<_>, rusqlite::Error>>()?;
let mut result = Vec::new();
let mut stmt =
tx.prepare("SELECT worktree_root FROM worktree_roots WHERE workspace_id = ?")?;
for workspace_id in workspace_ids {
let roots = stmt
.query_map([workspace_id.0], |row| {
let row = row.get::<_, Vec<u8>>(0)?;
Ok(PathBuf::from(OsStr::from_bytes(&row)).into())
// If you need to debug this, here's the string parsing:
// let row = row.get::<_, String>(0)?;
// Ok(PathBuf::from(row).into())
})?
.collect::<Result<Vec<_>, rusqlite::Error>>()?;
result.push((workspace_id, roots))
}
result
};
tx.commit()?;
return Ok(result);
}
self.real()
.map(|db| {
let mut lock = db.connection.lock();
match logic(&mut lock, limit) {
Ok(result) => result,
Err(err) => {
log::error!("Failed to get recent workspaces, err: {}", err);
Vec::new()
}
}
})
.unwrap_or_else(|| Vec::new())
let res = self.with_savepoint("recent_workspaces", |conn| {
let ids = conn.prepare("SELECT workspace_id FROM workspaces ORDER BY last_opened_timestamp DESC LIMIT ?")?
.bind(limit)?
.rows::<i64>()?
.iter()
.map(|row| WorkspaceId(*row));
let result = Vec::new();
let stmt = conn.prepare("SELECT worktree_root FROM worktree_roots WHERE workspace_id = ?")?;
for workspace_id in ids {
let roots = stmt.bind(workspace_id.0)?
.rows::<Vec<u8>>()?
.iter()
.map(|row| {
PathBuf::from(OsStr::from_bytes(&row)).into()
})
.collect();
result.push((workspace_id, roots))
}
Ok(result)
});
match res {
Ok(result) => result,
Err(err) => {
log::error!("Failed to get recent workspaces, err: {}", err);
Vec::new()
}
}
}
}
fn current_millis() -> Result<u64, anyhow::Error> {
// SQLite only supports u64 integers, which means this code will trigger
// undefined behavior in 584 million years. It's probably fine.
Ok(SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis() as u64)
}
fn update_worktree_roots<P>(
connection: &Connection,
workspace_id: &WorkspaceId,
@@ -265,33 +207,32 @@ where
if let Some(preexisting_id) = preexisting_id {
if preexisting_id != *workspace_id {
// Should also delete fields in other tables with cascading updates
connection.execute(
connection.prepare(
"DELETE FROM workspaces WHERE workspace_id = ?",
[preexisting_id.0],
)?;
)?
.bind(preexisting_id.0)?
.exec()?;
}
}
connection.execute(
"DELETE FROM worktree_roots WHERE workspace_id = ?",
[workspace_id.0],
)?;
connection
.prepare("DELETE FROM worktree_roots WHERE workspace_id = ?")?
.bind(workspace_id.0)?
.exec()?;
for root in worktree_roots {
let path = root.as_ref().as_os_str().as_bytes();
// If you need to debug this, here's the string parsing:
// let path = root.as_ref().to_string_lossy().to_string();
connection.execute(
"INSERT INTO worktree_roots(workspace_id, worktree_root) VALUES (?, ?)",
params![workspace_id.0, path],
)?;
connection.prepare("INSERT INTO worktree_roots(workspace_id, worktree_root) VALUES (?, ?)")?
.bind((workspace_id.0, path))?
.exec()?;
}
connection.execute(
"UPDATE workspaces SET last_opened_timestamp = ? WHERE workspace_id = ?",
params![current_millis()?, workspace_id.0],
)?;
connection.prepare("UPDATE workspaces SET last_opened_timestamp = CURRENT_TIMESTAMP WHERE workspace_id = ?")?
.bind(workspace_id.0)?
.exec()?;
Ok(())
}
@@ -300,13 +241,6 @@ fn get_workspace_id<P>(worktree_roots: &[P], connection: &Connection) -> Result<
where
P: AsRef<Path> + Debug,
{
// fn logic<P>(
// worktree_roots: &[P],
// connection: &Connection,
// ) -> Result<Option<WorkspaceId>, anyhow::Error>
// where
// P: AsRef<Path> + Debug,
// {
// Short circuit if we can
if worktree_roots.len() == 0 {
return Ok(None);
@@ -324,6 +258,7 @@ where
}
}
array_binding_stmt.push(')');
// Any workspace can have multiple independent paths, and these paths
// can overlap in the database. Take this test data for example:
//
@@ -393,43 +328,19 @@ where
// caching it.
let mut stmt = connection.prepare(&query)?;
// Make sure we bound the parameters correctly
debug_assert!(worktree_roots.len() + 1 == stmt.parameter_count());
debug_assert!(worktree_roots.len() as i32 + 1 == stmt.parameter_count());
for i in 0..worktree_roots.len() {
let path = &worktree_roots[i].as_ref().as_os_str().as_bytes();
// If you need to debug this, here's the string parsing:
// let path = &worktree_roots[i].as_ref().to_string_lossy().to_string()
stmt.raw_bind_parameter(i + 1, path)?
stmt.bind_value(*path, i as i32 + 1);
}
// No -1, because SQLite is 1 based
stmt.raw_bind_parameter(worktree_roots.len() + 1, worktree_roots.len())?;
stmt.bind_value(worktree_roots.len(), worktree_roots.len() as i32 + 1)?;
let mut rows = stmt.raw_query();
let row = rows.next();
let result = if let Ok(Some(row)) = row {
Ok(Some(WorkspaceId(row.get(0)?)))
} else {
Ok(None)
};
// Ensure that this query only returns one row. The PRIMARY KEY constraint should catch this case
// but this is here to catch if someone refactors that constraint out.
debug_assert!(matches!(rows.next(), Ok(None)));
result
// }
// match logic(worktree_roots, connection) {
// Ok(result) => result,
// Err(err) => {
// log::error!(
// "Failed to get the workspace ID for paths {:?}, err: {}",
// worktree_roots,
// err
// );
// None
// }
// }
stmt.maybe_row()
.map(|row| row.map(|id| WorkspaceId(id)))
}
#[cfg(test)]