zed/crates/editor/src/persistence.rs
Kirill Bulatov b85af0e533
Fall back to handling the abs path for external worktree entries (#19612)
Certain files like Rust stdlib ones can be opened by cmd-clicking on
terminal, editor contents, etc.

Those files will not belong to the current worktree, so a fake worktree,
with a single file, invisible (i.e. its dir(s) will not be shown in the
UI such as project panel), will be created on the file opening.

When the file is closed, the worktree is closed and removed along the
way, so those worktrees are considered ephemeral and their ids are not
stored in the database.
This causes issues on reopening such files when they are closed. 

The PR makes Zed to fall back to opening the file by abs path when it's
not in the project metadata, but has the abs path stored in history or
in the opened items DB data.

Release Notes:

- Handle external worktree entries [re]open better
2024-10-23 17:34:23 +03:00

301 lines
9.9 KiB
Rust

use anyhow::Result;
use db::sqlez::bindable::{Bind, Column, StaticColumnCount};
use db::sqlez::statement::Statement;
use std::path::PathBuf;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use db::sqlez_macros::sql;
use db::{define_connection, query};
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<SystemTime>,
}
impl StaticColumnCount for SerializedEditor {
fn column_count() -> usize {
5
}
}
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.contents, start_index)?;
let start_index = statement.bind(&self.language, start_index)?;
let mtime = self.mtime.and_then(|mtime| {
mtime
.duration_since(UNIX_EPOCH)
.ok()
.map(|duration| (duration.as_secs() as i64, duration.subsec_nanos() as i32))
});
let start_index = match mtime {
Some((seconds, nanos)) => {
let start_index = statement.bind(&seconds, start_index)?;
statement.bind(&nanos, 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 (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)| UNIX_EPOCH + Duration::new(seconds as u64, nanos as u32));
let editor = Self {
abs_path,
contents,
language,
mtime,
};
Ok((editor, start_index))
}
}
define_connection!(
// 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,
// content: Option<String>,
// language: Option<String>,
// mtime_seconds: Option<i64>,
// mtime_nanos: Option<i32>,
// )
pub static ref DB: EditorDb<WorkspaceDb> =
&[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;
),
];
);
impl EditorDb {
query! {
pub fn get_serialized_editor(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<SerializedEditor>> {
SELECT 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, contents, language, mtime_seconds, mtime_nanos)
VALUES
(?1, ?2, ?3, ?4, ?5, ?6, ?7)
ON CONFLICT DO UPDATE SET
item_id = ?1,
workspace_id = ?2,
path = ?3,
contents = ?4,
language = ?5,
mtime_seconds = ?6,
mtime_nanos = ?7
}
}
// Returns the scroll top row, and offset
query! {
pub fn get_scroll_position(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<(u32, f32, f32)>> {
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: f32,
horizontal_offset: f32
) -> 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
}
}
pub async fn delete_unloaded_items(
&self,
workspace: WorkspaceId,
alive_items: Vec<ItemId>,
) -> Result<()> {
let placeholders = alive_items
.iter()
.map(|_| "?")
.collect::<Vec<&str>>()
.join(", ");
let query = format!(
"DELETE FROM editors WHERE workspace_id = ? AND item_id NOT IN ({placeholders})"
);
self.write(move |conn| {
let mut statement = Statement::prepare(conn, query)?;
let mut next_index = statement.bind(&workspace, 1)?;
for id in alive_items {
next_index = statement.bind(&id, next_index)?;
}
statement.exec()
})
.await
}
}
#[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 now = SystemTime::now();
let serialized_editor = SerializedEditor {
abs_path: None,
contents: None,
language: None,
mtime: Some(now),
};
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);
}
}