1use anyhow::Result;
2use db::sqlez::bindable::{Bind, Column, StaticColumnCount};
3use db::sqlez::statement::Statement;
4use std::path::PathBuf;
5use std::time::{Duration, SystemTime, UNIX_EPOCH};
6
7use db::sqlez_macros::sql;
8use db::{define_connection, query};
9
10use workspace::{ItemId, WorkspaceDb, WorkspaceId};
11
12#[derive(Clone, Debug, PartialEq, Default)]
13pub(crate) struct SerializedEditor {
14 pub(crate) abs_path: Option<PathBuf>,
15 pub(crate) contents: Option<String>,
16 pub(crate) language: Option<String>,
17 pub(crate) mtime: Option<SystemTime>,
18}
19
20impl StaticColumnCount for SerializedEditor {
21 fn column_count() -> usize {
22 5
23 }
24}
25
26impl Bind for SerializedEditor {
27 fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
28 let start_index = statement.bind(&self.abs_path, start_index)?;
29 let start_index = statement.bind(&self.contents, start_index)?;
30 let start_index = statement.bind(&self.language, start_index)?;
31
32 let mtime = self.mtime.and_then(|mtime| {
33 mtime
34 .duration_since(UNIX_EPOCH)
35 .ok()
36 .map(|duration| (duration.as_secs() as i64, duration.subsec_nanos() as i32))
37 });
38 let start_index = match mtime {
39 Some((seconds, nanos)) => {
40 let start_index = statement.bind(&seconds, start_index)?;
41 statement.bind(&nanos, start_index)?
42 }
43 None => {
44 let start_index = statement.bind::<Option<i64>>(&None, start_index)?;
45 statement.bind::<Option<i32>>(&None, start_index)?
46 }
47 };
48 Ok(start_index)
49 }
50}
51
52impl Column for SerializedEditor {
53 fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
54 let (abs_path, start_index): (Option<PathBuf>, i32) =
55 Column::column(statement, start_index)?;
56 let (contents, start_index): (Option<String>, i32) =
57 Column::column(statement, start_index)?;
58 let (language, start_index): (Option<String>, i32) =
59 Column::column(statement, start_index)?;
60 let (mtime_seconds, start_index): (Option<i64>, i32) =
61 Column::column(statement, start_index)?;
62 let (mtime_nanos, start_index): (Option<i32>, i32) =
63 Column::column(statement, start_index)?;
64
65 let mtime = mtime_seconds
66 .zip(mtime_nanos)
67 .map(|(seconds, nanos)| UNIX_EPOCH + Duration::new(seconds as u64, nanos as u32));
68
69 let editor = Self {
70 abs_path,
71 contents,
72 language,
73 mtime,
74 };
75 Ok((editor, start_index))
76 }
77}
78
79define_connection!(
80 // Current schema shape using pseudo-rust syntax:
81 // editors(
82 // item_id: usize,
83 // workspace_id: usize,
84 // path: Option<PathBuf>,
85 // scroll_top_row: usize,
86 // scroll_vertical_offset: f32,
87 // scroll_horizontal_offset: f32,
88 // content: Option<String>,
89 // language: Option<String>,
90 // mtime_seconds: Option<i64>,
91 // mtime_nanos: Option<i32>,
92 // )
93 pub static ref DB: EditorDb<WorkspaceDb> =
94 &[sql! (
95 CREATE TABLE editors(
96 item_id INTEGER NOT NULL,
97 workspace_id INTEGER NOT NULL,
98 path BLOB NOT NULL,
99 PRIMARY KEY(item_id, workspace_id),
100 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
101 ON DELETE CASCADE
102 ON UPDATE CASCADE
103 ) STRICT;
104 ),
105 sql! (
106 ALTER TABLE editors ADD COLUMN scroll_top_row INTEGER NOT NULL DEFAULT 0;
107 ALTER TABLE editors ADD COLUMN scroll_horizontal_offset REAL NOT NULL DEFAULT 0;
108 ALTER TABLE editors ADD COLUMN scroll_vertical_offset REAL NOT NULL DEFAULT 0;
109 ),
110 sql! (
111 // Since sqlite3 doesn't support ALTER COLUMN, we create a new
112 // table, move the data over, drop the old table, rename new table.
113 CREATE TABLE new_editors_tmp (
114 item_id INTEGER NOT NULL,
115 workspace_id INTEGER NOT NULL,
116 path BLOB, // <-- No longer "NOT NULL"
117 scroll_top_row INTEGER NOT NULL DEFAULT 0,
118 scroll_horizontal_offset REAL NOT NULL DEFAULT 0,
119 scroll_vertical_offset REAL NOT NULL DEFAULT 0,
120 contents TEXT, // New
121 language TEXT, // New
122 PRIMARY KEY(item_id, workspace_id),
123 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
124 ON DELETE CASCADE
125 ON UPDATE CASCADE
126 ) STRICT;
127
128 INSERT INTO new_editors_tmp(item_id, workspace_id, path, scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset)
129 SELECT item_id, workspace_id, path, scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset
130 FROM editors;
131
132 DROP TABLE editors;
133
134 ALTER TABLE new_editors_tmp RENAME TO editors;
135 ),
136 sql! (
137 ALTER TABLE editors ADD COLUMN mtime_seconds INTEGER DEFAULT NULL;
138 ALTER TABLE editors ADD COLUMN mtime_nanos INTEGER DEFAULT NULL;
139 ),
140 ];
141);
142
143impl EditorDb {
144 query! {
145 pub fn get_serialized_editor(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<SerializedEditor>> {
146 SELECT path, contents, language, mtime_seconds, mtime_nanos FROM editors
147 WHERE item_id = ? AND workspace_id = ?
148 }
149 }
150
151 query! {
152 pub async fn save_serialized_editor(item_id: ItemId, workspace_id: WorkspaceId, serialized_editor: SerializedEditor) -> Result<()> {
153 INSERT INTO editors
154 (item_id, workspace_id, path, contents, language, mtime_seconds, mtime_nanos)
155 VALUES
156 (?1, ?2, ?3, ?4, ?5, ?6, ?7)
157 ON CONFLICT DO UPDATE SET
158 item_id = ?1,
159 workspace_id = ?2,
160 path = ?3,
161 contents = ?4,
162 language = ?5,
163 mtime_seconds = ?6,
164 mtime_nanos = ?7
165 }
166 }
167
168 // Returns the scroll top row, and offset
169 query! {
170 pub fn get_scroll_position(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<(u32, f32, f32)>> {
171 SELECT scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset
172 FROM editors
173 WHERE item_id = ? AND workspace_id = ?
174 }
175 }
176
177 query! {
178 pub async fn save_scroll_position(
179 item_id: ItemId,
180 workspace_id: WorkspaceId,
181 top_row: u32,
182 vertical_offset: f32,
183 horizontal_offset: f32
184 ) -> Result<()> {
185 UPDATE OR IGNORE editors
186 SET
187 scroll_top_row = ?3,
188 scroll_horizontal_offset = ?4,
189 scroll_vertical_offset = ?5
190 WHERE item_id = ?1 AND workspace_id = ?2
191 }
192 }
193
194 pub async fn delete_unloaded_items(
195 &self,
196 workspace: WorkspaceId,
197 alive_items: Vec<ItemId>,
198 ) -> Result<()> {
199 let placeholders = alive_items
200 .iter()
201 .map(|_| "?")
202 .collect::<Vec<&str>>()
203 .join(", ");
204
205 let query = format!(
206 "DELETE FROM editors WHERE workspace_id = ? AND item_id NOT IN ({placeholders})"
207 );
208
209 self.write(move |conn| {
210 let mut statement = Statement::prepare(conn, query)?;
211 let mut next_index = statement.bind(&workspace, 1)?;
212 for id in alive_items {
213 next_index = statement.bind(&id, next_index)?;
214 }
215 statement.exec()
216 })
217 .await
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 #[gpui::test]
226 async fn test_save_and_get_serialized_editor() {
227 let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
228
229 let serialized_editor = SerializedEditor {
230 abs_path: Some(PathBuf::from("testing.txt")),
231 contents: None,
232 language: None,
233 mtime: None,
234 };
235
236 DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
237 .await
238 .unwrap();
239
240 let have = DB
241 .get_serialized_editor(1234, workspace_id)
242 .unwrap()
243 .unwrap();
244 assert_eq!(have, serialized_editor);
245
246 // Now update contents and language
247 let serialized_editor = SerializedEditor {
248 abs_path: Some(PathBuf::from("testing.txt")),
249 contents: Some("Test".to_owned()),
250 language: Some("Go".to_owned()),
251 mtime: None,
252 };
253
254 DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
255 .await
256 .unwrap();
257
258 let have = DB
259 .get_serialized_editor(1234, workspace_id)
260 .unwrap()
261 .unwrap();
262 assert_eq!(have, serialized_editor);
263
264 // Now set all the fields to NULL
265 let serialized_editor = SerializedEditor {
266 abs_path: None,
267 contents: None,
268 language: None,
269 mtime: None,
270 };
271
272 DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
273 .await
274 .unwrap();
275
276 let have = DB
277 .get_serialized_editor(1234, workspace_id)
278 .unwrap()
279 .unwrap();
280 assert_eq!(have, serialized_editor);
281
282 // Storing and retrieving mtime
283 let now = SystemTime::now();
284 let serialized_editor = SerializedEditor {
285 abs_path: None,
286 contents: None,
287 language: None,
288 mtime: Some(now),
289 };
290
291 DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
292 .await
293 .unwrap();
294
295 let have = DB
296 .get_serialized_editor(1234, workspace_id)
297 .unwrap()
298 .unwrap();
299 assert_eq!(have, serialized_editor);
300 }
301}