1use anyhow::Result;
2use db::sqlez::statement::Statement;
3use std::path::PathBuf;
4
5use db::sqlez_macros::sql;
6use db::{define_connection, query};
7
8use workspace::{ItemId, WorkspaceDb, WorkspaceId};
9
10define_connection!(
11 // Current schema shape using pseudo-rust syntax:
12 // editors(
13 // item_id: usize,
14 // workspace_id: usize,
15 // path: Option<PathBuf>,
16 // scroll_top_row: usize,
17 // scroll_vertical_offset: f32,
18 // scroll_horizontal_offset: f32,
19 // content: Option<String>,
20 // language: Option<String>,
21 // )
22 pub static ref DB: EditorDb<WorkspaceDb> =
23 &[sql! (
24 CREATE TABLE editors(
25 item_id INTEGER NOT NULL,
26 workspace_id INTEGER NOT NULL,
27 path BLOB NOT NULL,
28 PRIMARY KEY(item_id, workspace_id),
29 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
30 ON DELETE CASCADE
31 ON UPDATE CASCADE
32 ) STRICT;
33 ),
34 sql! (
35 ALTER TABLE editors ADD COLUMN scroll_top_row INTEGER NOT NULL DEFAULT 0;
36 ALTER TABLE editors ADD COLUMN scroll_horizontal_offset REAL NOT NULL DEFAULT 0;
37 ALTER TABLE editors ADD COLUMN scroll_vertical_offset REAL NOT NULL DEFAULT 0;
38 ),
39 sql! (
40 // Since sqlite3 doesn't support ALTER COLUMN, we create a new
41 // table, move the data over, drop the old table, rename new table.
42 CREATE TABLE new_editors_tmp (
43 item_id INTEGER NOT NULL,
44 workspace_id INTEGER NOT NULL,
45 path BLOB, // <-- No longer "NOT NULL"
46 scroll_top_row INTEGER NOT NULL DEFAULT 0,
47 scroll_horizontal_offset REAL NOT NULL DEFAULT 0,
48 scroll_vertical_offset REAL NOT NULL DEFAULT 0,
49 contents TEXT, // New
50 language TEXT, // New
51 PRIMARY KEY(item_id, workspace_id),
52 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
53 ON DELETE CASCADE
54 ON UPDATE CASCADE
55 ) STRICT;
56
57 INSERT INTO new_editors_tmp(item_id, workspace_id, path, scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset)
58 SELECT item_id, workspace_id, path, scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset
59 FROM editors;
60
61 DROP TABLE editors;
62
63 ALTER TABLE new_editors_tmp RENAME TO editors;
64 )];
65);
66
67impl EditorDb {
68 query! {
69 pub fn get_path_and_contents(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<(Option<PathBuf>, Option<String>, Option<String>)>> {
70 SELECT path, contents, language FROM editors
71 WHERE item_id = ? AND workspace_id = ?
72 }
73 }
74
75 query! {
76 pub async fn save_path(item_id: ItemId, workspace_id: WorkspaceId, path: PathBuf) -> Result<()> {
77 INSERT INTO editors
78 (item_id, workspace_id, path)
79 VALUES
80 (?1, ?2, ?3)
81 ON CONFLICT DO UPDATE SET
82 item_id = ?1,
83 workspace_id = ?2,
84 path = ?3
85 }
86 }
87
88 query! {
89 pub async fn save_contents(item_id: ItemId, workspace: WorkspaceId, contents: Option<String>, language: Option<String>) -> Result<()> {
90 INSERT INTO editors
91 (item_id, workspace_id, contents, language)
92 VALUES
93 (?1, ?2, ?3, ?4)
94 ON CONFLICT DO UPDATE SET
95 item_id = ?1,
96 workspace_id = ?2,
97 contents = ?3,
98 language = ?4
99 }
100 }
101
102 // Returns the scroll top row, and offset
103 query! {
104 pub fn get_scroll_position(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<(u32, f32, f32)>> {
105 SELECT scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset
106 FROM editors
107 WHERE item_id = ? AND workspace_id = ?
108 }
109 }
110
111 query! {
112 pub async fn save_scroll_position(
113 item_id: ItemId,
114 workspace_id: WorkspaceId,
115 top_row: u32,
116 vertical_offset: f32,
117 horizontal_offset: f32
118 ) -> Result<()> {
119 UPDATE OR IGNORE editors
120 SET
121 scroll_top_row = ?3,
122 scroll_horizontal_offset = ?4,
123 scroll_vertical_offset = ?5
124 WHERE item_id = ?1 AND workspace_id = ?2
125 }
126 }
127
128 pub async fn delete_unloaded_items(
129 &self,
130 workspace: WorkspaceId,
131 alive_items: Vec<ItemId>,
132 ) -> Result<()> {
133 let placeholders = alive_items
134 .iter()
135 .map(|_| "?")
136 .collect::<Vec<&str>>()
137 .join(", ");
138
139 let query = format!(
140 "DELETE FROM editors WHERE workspace_id = ? AND item_id NOT IN ({placeholders})"
141 );
142
143 self.write(move |conn| {
144 let mut statement = Statement::prepare(conn, query)?;
145 let mut next_index = statement.bind(&workspace, 1)?;
146 for id in alive_items {
147 next_index = statement.bind(&id, next_index)?;
148 }
149 statement.exec()
150 })
151 .await
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158 use gpui;
159
160 #[gpui::test]
161 async fn test_saving_content() {
162 env_logger::try_init().ok();
163
164 let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
165
166 // Sanity check: make sure there is no row in the `editors` table
167 assert_eq!(DB.get_path_and_contents(1234, workspace_id).unwrap(), None);
168
169 // Save content/language
170 DB.save_contents(
171 1234,
172 workspace_id,
173 Some("testing".into()),
174 Some("Go".into()),
175 )
176 .await
177 .unwrap();
178
179 // Check that it can be read from DB
180 let path_and_contents = DB.get_path_and_contents(1234, workspace_id).unwrap();
181 let (path, contents, language) = path_and_contents.unwrap();
182 assert!(path.is_none());
183 assert_eq!(contents, Some("testing".to_owned()));
184 assert_eq!(language, Some("Go".to_owned()));
185
186 // Update it with NULL
187 DB.save_contents(1234, workspace_id, None, None)
188 .await
189 .unwrap();
190
191 // Check that it worked
192 let path_and_contents = DB.get_path_and_contents(1234, workspace_id).unwrap();
193 let (path, contents, language) = path_and_contents.unwrap();
194 assert!(path.is_none());
195 assert!(contents.is_none());
196 assert!(language.is_none());
197 }
198}