593 lines · 19.8 KB · Rust
RawDownload
use anyhow::Result;use db::{    query,    sqlez::{        bindable::{Bind, Column, StaticColumnCount},        domain::Domain,        statement::Statement,    },    sqlez_macros::sql,};use fs::MTime;use itertools::Itertools as _;use std::path::PathBuf; use workspace::{ItemId, WorkspaceDb, WorkspaceId}; #[derive(Clone, Debug, PartialEq, Default)]pub(crate) struct SerializedEditor {    pub(crate) abs_path: Option<PathBuf>,    pub(crate) contents: Option<String>,    pub(crate) language: Option<String>,    pub(crate) mtime: Option<MTime>,} impl StaticColumnCount for SerializedEditor {    fn column_count() -> usize {        6    }} impl Bind for SerializedEditor {    fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {        let start_index = statement.bind(&self.abs_path, start_index)?;        let start_index = statement.bind(            &self                .abs_path                .as_ref()                .map(|p| p.to_string_lossy().into_owned()),            start_index,        )?;        let start_index = statement.bind(&self.contents, start_index)?;        let start_index = statement.bind(&self.language, start_index)?;         let start_index = match self            .mtime            .and_then(|mtime| mtime.to_seconds_and_nanos_for_persistence())        {            Some((seconds, nanos)) => {                let start_index = statement.bind(&(seconds as i64), start_index)?;                statement.bind(&(nanos as i32), start_index)?            }            None => {                let start_index = statement.bind::<Option<i64>>(&None, start_index)?;                statement.bind::<Option<i32>>(&None, start_index)?            }        };        Ok(start_index)    }} impl Column for SerializedEditor {    fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {        let (abs_path, start_index): (Option<PathBuf>, i32) =            Column::column(statement, start_index)?;        let (_abs_path, start_index): (Option<PathBuf>, i32) =            Column::column(statement, start_index)?;        let (contents, start_index): (Option<String>, i32) =            Column::column(statement, start_index)?;        let (language, start_index): (Option<String>, i32) =            Column::column(statement, start_index)?;        let (mtime_seconds, start_index): (Option<i64>, i32) =            Column::column(statement, start_index)?;        let (mtime_nanos, start_index): (Option<i32>, i32) =            Column::column(statement, start_index)?;         let mtime = mtime_seconds            .zip(mtime_nanos)            .map(|(seconds, nanos)| MTime::from_seconds_and_nanos(seconds as u64, nanos as u32));         let editor = Self {            abs_path,            contents,            language,            mtime,        };        Ok((editor, start_index))    }} pub struct EditorDb(db::sqlez::thread_safe_connection::ThreadSafeConnection); impl Domain for EditorDb {    const NAME: &str = stringify!(EditorDb);     // Current schema shape using pseudo-rust syntax:    // editors(    //   item_id: usize,    //   workspace_id: usize,    //   path: Option<PathBuf>,    //   scroll_top_row: usize,    //   scroll_vertical_offset: f32,    //   scroll_horizontal_offset: f32,    //   contents: Option<String>,    //   language: Option<String>,    //   mtime_seconds: Option<i64>,    //   mtime_nanos: Option<i32>,    // )    //    // editor_selections(    //   item_id: usize,    //   editor_id: usize,    //   workspace_id: usize,    //   start: usize,    //   end: usize,    // )    //    // editor_folds(    //   item_id: usize,    //   editor_id: usize,    //   workspace_id: usize,    //   start: usize,    //   end: usize,    //   start_fingerprint: Option<String>,    //   end_fingerprint: Option<String>,    // )     const MIGRATIONS: &[&str] = &[        sql! (            CREATE TABLE editors(                item_id INTEGER NOT NULL,                workspace_id INTEGER NOT NULL,                path BLOB NOT NULL,                PRIMARY KEY(item_id, workspace_id),                FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)                ON DELETE CASCADE                ON UPDATE CASCADE            ) STRICT;        ),        sql! (            ALTER TABLE editors ADD COLUMN scroll_top_row INTEGER NOT NULL DEFAULT 0;            ALTER TABLE editors ADD COLUMN scroll_horizontal_offset REAL NOT NULL DEFAULT 0;            ALTER TABLE editors ADD COLUMN scroll_vertical_offset REAL NOT NULL DEFAULT 0;        ),        sql! (            // Since sqlite3 doesn't support ALTER COLUMN, we create a new            // table, move the data over, drop the old table, rename new table.            CREATE TABLE new_editors_tmp (                item_id INTEGER NOT NULL,                workspace_id INTEGER NOT NULL,                path BLOB, // <-- No longer "NOT NULL"                scroll_top_row INTEGER NOT NULL DEFAULT 0,                scroll_horizontal_offset REAL NOT NULL DEFAULT 0,                scroll_vertical_offset REAL NOT NULL DEFAULT 0,                contents TEXT, // New                language TEXT, // New                PRIMARY KEY(item_id, workspace_id),                FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)                ON DELETE CASCADE                ON UPDATE CASCADE            ) STRICT;             INSERT INTO new_editors_tmp(item_id, workspace_id, path, scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset)            SELECT item_id, workspace_id, path, scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset            FROM editors;             DROP TABLE editors;             ALTER TABLE new_editors_tmp RENAME TO editors;        ),        sql! (            ALTER TABLE editors ADD COLUMN mtime_seconds INTEGER DEFAULT NULL;            ALTER TABLE editors ADD COLUMN mtime_nanos INTEGER DEFAULT NULL;        ),        sql! (            CREATE TABLE editor_selections (                item_id INTEGER NOT NULL,                editor_id INTEGER NOT NULL,                workspace_id INTEGER NOT NULL,                start INTEGER NOT NULL,                end INTEGER NOT NULL,                PRIMARY KEY(item_id),                FOREIGN KEY(editor_id, workspace_id) REFERENCES editors(item_id, workspace_id)                ON DELETE CASCADE            ) STRICT;        ),        sql! (            ALTER TABLE editors ADD COLUMN buffer_path TEXT;            UPDATE editors SET buffer_path = CAST(path AS TEXT);        ),        sql! (            CREATE TABLE editor_folds (                item_id INTEGER NOT NULL,                editor_id INTEGER NOT NULL,                workspace_id INTEGER NOT NULL,                start INTEGER NOT NULL,                end INTEGER NOT NULL,                PRIMARY KEY(item_id),                FOREIGN KEY(editor_id, workspace_id) REFERENCES editors(item_id, workspace_id)                ON DELETE CASCADE            ) STRICT;        ),        sql! (            ALTER TABLE editor_folds ADD COLUMN start_fingerprint TEXT;            ALTER TABLE editor_folds ADD COLUMN end_fingerprint TEXT;        ),    ];} db::static_connection!(DB, EditorDb, [WorkspaceDb]); // https://www.sqlite.org/limits.html// > <..> the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER,// > which defaults to <..> 32766 for SQLite versions after 3.32.0.const MAX_QUERY_PLACEHOLDERS: usize = 32000; impl EditorDb {    query! {        pub fn get_serialized_editor(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<SerializedEditor>> {            SELECT path, buffer_path, contents, language, mtime_seconds, mtime_nanos FROM editors            WHERE item_id = ? AND workspace_id = ?        }    }     query! {        pub async fn save_serialized_editor(item_id: ItemId, workspace_id: WorkspaceId, serialized_editor: SerializedEditor) -> Result<()> {            INSERT INTO editors                (item_id, workspace_id, path, buffer_path, contents, language, mtime_seconds, mtime_nanos)            VALUES                (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)            ON CONFLICT DO UPDATE SET                item_id = ?1,                workspace_id = ?2,                path = ?3,                buffer_path = ?4,                contents = ?5,                language = ?6,                mtime_seconds = ?7,                mtime_nanos = ?8        }    }     // Returns the scroll top row, and offset    query! {        pub fn get_scroll_position(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<(u32, f64, f64)>> {            SELECT scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset            FROM editors            WHERE item_id = ? AND workspace_id = ?        }    }     query! {        pub async fn save_scroll_position(            item_id: ItemId,            workspace_id: WorkspaceId,            top_row: u32,            vertical_offset: f64,            horizontal_offset: f64        ) -> Result<()> {            UPDATE OR IGNORE editors            SET                scroll_top_row = ?3,                scroll_horizontal_offset = ?4,                scroll_vertical_offset = ?5            WHERE item_id = ?1 AND workspace_id = ?2        }    }     query! {        pub fn get_editor_selections(            editor_id: ItemId,            workspace_id: WorkspaceId        ) -> Result<Vec<(usize, usize)>> {            SELECT start, end            FROM editor_selections            WHERE editor_id = ?1 AND workspace_id = ?2        }    }     query! {        pub fn get_editor_folds(            editor_id: ItemId,            workspace_id: WorkspaceId        ) -> Result<Vec<(usize, usize, Option<String>, Option<String>)>> {            SELECT start, end, start_fingerprint, end_fingerprint            FROM editor_folds            WHERE editor_id = ?1 AND workspace_id = ?2        }    }     // Migrate folds from an old editor_id to a new one.    // This is needed because entity IDs change between sessions, but workspace    // cleanup deletes old editor rows (cascading to folds) before the new    // editor has a chance to re-save its folds.    //    // We temporarily disable FK checks because the new editor row doesn't exist    // yet (it gets created during workspace serialization, which runs later).    pub async fn migrate_editor_folds(        &self,        old_editor_id: ItemId,        new_editor_id: ItemId,        workspace_id: WorkspaceId,    ) -> Result<()> {        self.write(move |conn| {            let _ = conn.exec("PRAGMA foreign_keys = OFF");            let mut statement = Statement::prepare(                conn,                "UPDATE editor_folds SET editor_id = ?2 WHERE editor_id = ?1 AND workspace_id = ?3",            )?;            statement.bind(&old_editor_id, 1)?;            statement.bind(&new_editor_id, 2)?;            statement.bind(&workspace_id, 3)?;            let result = statement.exec();            let _ = conn.exec("PRAGMA foreign_keys = ON");            result        })        .await    }     pub async fn save_editor_selections(        &self,        editor_id: ItemId,        workspace_id: WorkspaceId,        selections: Vec<(usize, usize)>,    ) -> Result<()> {        log::debug!("Saving selections for editor {editor_id} in workspace {workspace_id:?}");        let mut first_selection;        let mut last_selection = 0_usize;        for (count, placeholders) in std::iter::once("(?1, ?2, ?, ?)")            .cycle()            .take(selections.len())            .chunks(MAX_QUERY_PLACEHOLDERS / 4)            .into_iter()            .map(|chunk| {                let mut count = 0;                let placeholders = chunk                    .inspect(|_| {                        count += 1;                    })                    .join(", ");                (count, placeholders)            })            .collect::<Vec<_>>()        {            first_selection = last_selection;            last_selection = last_selection + count;            let query = format!(                r#"DELETE FROM editor_selections WHERE editor_id = ?1 AND workspace_id = ?2; INSERT OR IGNORE INTO editor_selections (editor_id, workspace_id, start, end)VALUES {placeholders};"#            );             let selections = selections[first_selection..last_selection].to_vec();            self.write(move |conn| {                let mut statement = Statement::prepare(conn, query)?;                statement.bind(&editor_id, 1)?;                let mut next_index = statement.bind(&workspace_id, 2)?;                for (start, end) in selections {                    next_index = statement.bind(&start, next_index)?;                    next_index = statement.bind(&end, next_index)?;                }                statement.exec()            })            .await?;        }        Ok(())    }     pub async fn save_editor_folds(        &self,        editor_id: ItemId,        workspace_id: WorkspaceId,        folds: Vec<(usize, usize, String, String)>,    ) -> Result<()> {        log::debug!("Saving folds for editor {editor_id} in workspace {workspace_id:?}");        let mut first_fold;        let mut last_fold = 0_usize;        for (count, placeholders) in std::iter::once("(?1, ?2, ?, ?, ?, ?)")            .cycle()            .take(folds.len())            .chunks(MAX_QUERY_PLACEHOLDERS / 6)            .into_iter()            .map(|chunk| {                let mut count = 0;                let placeholders = chunk                    .inspect(|_| {                        count += 1;                    })                    .join(", ");                (count, placeholders)            })            .collect::<Vec<_>>()        {            first_fold = last_fold;            last_fold = last_fold + count;            let query = format!(                r#"DELETE FROM editor_folds WHERE editor_id = ?1 AND workspace_id = ?2; INSERT OR IGNORE INTO editor_folds (editor_id, workspace_id, start, end, start_fingerprint, end_fingerprint)VALUES {placeholders};"#            );             let folds = folds[first_fold..last_fold].to_vec();            self.write(move |conn| {                let mut statement = Statement::prepare(conn, query)?;                statement.bind(&editor_id, 1)?;                let mut next_index = statement.bind(&workspace_id, 2)?;                for (start, end, start_fp, end_fp) in folds {                    next_index = statement.bind(&start, next_index)?;                    next_index = statement.bind(&end, next_index)?;                    next_index = statement.bind(&start_fp, next_index)?;                    next_index = statement.bind(&end_fp, next_index)?;                }                statement.exec()            })            .await?;        }        Ok(())    }} #[cfg(test)]mod tests {    use super::*;     #[gpui::test]    async fn test_save_and_get_serialized_editor() {        let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();         let serialized_editor = SerializedEditor {            abs_path: Some(PathBuf::from("testing.txt")),            contents: None,            language: None,            mtime: None,        };         DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())            .await            .unwrap();         let have = DB            .get_serialized_editor(1234, workspace_id)            .unwrap()            .unwrap();        assert_eq!(have, serialized_editor);         // Now update contents and language        let serialized_editor = SerializedEditor {            abs_path: Some(PathBuf::from("testing.txt")),            contents: Some("Test".to_owned()),            language: Some("Go".to_owned()),            mtime: None,        };         DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())            .await            .unwrap();         let have = DB            .get_serialized_editor(1234, workspace_id)            .unwrap()            .unwrap();        assert_eq!(have, serialized_editor);         // Now set all the fields to NULL        let serialized_editor = SerializedEditor {            abs_path: None,            contents: None,            language: None,            mtime: None,        };         DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())            .await            .unwrap();         let have = DB            .get_serialized_editor(1234, workspace_id)            .unwrap()            .unwrap();        assert_eq!(have, serialized_editor);         // Storing and retrieving mtime        let serialized_editor = SerializedEditor {            abs_path: None,            contents: None,            language: None,            mtime: Some(MTime::from_seconds_and_nanos(100, 42)),        };         DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())            .await            .unwrap();         let have = DB            .get_serialized_editor(1234, workspace_id)

Lines 1–500 of 593.