1use anyhow::Result;
2use db::sqlez::bindable::{Bind, Column, StaticColumnCount};
3use db::sqlez::statement::Statement;
4use fs::MTime;
5use itertools::Itertools as _;
6use std::path::PathBuf;
7
8use db::sqlez_macros::sql;
9use db::{define_connection, query};
10
11use workspace::{ItemId, WorkspaceDb, WorkspaceId};
12
13#[derive(Clone, Debug, PartialEq, Default)]
14pub(crate) struct SerializedEditor {
15 pub(crate) abs_path: Option<PathBuf>,
16 pub(crate) contents: Option<String>,
17 pub(crate) language: Option<String>,
18 pub(crate) mtime: Option<MTime>,
19}
20
21impl StaticColumnCount for SerializedEditor {
22 fn column_count() -> usize {
23 6
24 }
25}
26
27impl Bind for SerializedEditor {
28 fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
29 let start_index = statement.bind(&self.abs_path, start_index)?;
30 let start_index = statement.bind(
31 &self
32 .abs_path
33 .as_ref()
34 .map(|p| p.to_string_lossy().to_string()),
35 start_index,
36 )?;
37 let start_index = statement.bind(&self.contents, start_index)?;
38 let start_index = statement.bind(&self.language, start_index)?;
39
40 let start_index = match self
41 .mtime
42 .and_then(|mtime| mtime.to_seconds_and_nanos_for_persistence())
43 {
44 Some((seconds, nanos)) => {
45 let start_index = statement.bind(&(seconds as i64), start_index)?;
46 statement.bind(&(nanos as i32), start_index)?
47 }
48 None => {
49 let start_index = statement.bind::<Option<i64>>(&None, start_index)?;
50 statement.bind::<Option<i32>>(&None, start_index)?
51 }
52 };
53 Ok(start_index)
54 }
55}
56
57impl Column for SerializedEditor {
58 fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
59 let (abs_path, start_index): (Option<PathBuf>, i32) =
60 Column::column(statement, start_index)?;
61 let (_abs_path, start_index): (Option<PathBuf>, i32) =
62 Column::column(statement, start_index)?;
63 let (contents, start_index): (Option<String>, i32) =
64 Column::column(statement, start_index)?;
65 let (language, start_index): (Option<String>, i32) =
66 Column::column(statement, start_index)?;
67 let (mtime_seconds, start_index): (Option<i64>, i32) =
68 Column::column(statement, start_index)?;
69 let (mtime_nanos, start_index): (Option<i32>, i32) =
70 Column::column(statement, start_index)?;
71
72 let mtime = mtime_seconds
73 .zip(mtime_nanos)
74 .map(|(seconds, nanos)| MTime::from_seconds_and_nanos(seconds as u64, nanos as u32));
75
76 let editor = Self {
77 abs_path,
78 contents,
79 language,
80 mtime,
81 };
82 Ok((editor, start_index))
83 }
84}
85
86define_connection!(
87 // Current schema shape using pseudo-rust syntax:
88 // editors(
89 // item_id: usize,
90 // workspace_id: usize,
91 // path: Option<PathBuf>,
92 // scroll_top_row: usize,
93 // scroll_vertical_offset: f32,
94 // scroll_horizontal_offset: f32,
95 // contents: Option<String>,
96 // language: Option<String>,
97 // mtime_seconds: Option<i64>,
98 // mtime_nanos: Option<i32>,
99 // )
100 //
101 // editor_selections(
102 // item_id: usize,
103 // editor_id: usize,
104 // workspace_id: usize,
105 // start: usize,
106 // end: usize,
107 // )
108 //
109 // editor_folds(
110 // item_id: usize,
111 // editor_id: usize,
112 // workspace_id: usize,
113 // start: usize,
114 // end: usize,
115 // )
116 pub static ref DB: EditorDb<WorkspaceDb> = &[
117 sql! (
118 CREATE TABLE editors(
119 item_id INTEGER NOT NULL,
120 workspace_id INTEGER NOT NULL,
121 path BLOB NOT NULL,
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 sql! (
129 ALTER TABLE editors ADD COLUMN scroll_top_row INTEGER NOT NULL DEFAULT 0;
130 ALTER TABLE editors ADD COLUMN scroll_horizontal_offset REAL NOT NULL DEFAULT 0;
131 ALTER TABLE editors ADD COLUMN scroll_vertical_offset REAL NOT NULL DEFAULT 0;
132 ),
133 sql! (
134 // Since sqlite3 doesn't support ALTER COLUMN, we create a new
135 // table, move the data over, drop the old table, rename new table.
136 CREATE TABLE new_editors_tmp (
137 item_id INTEGER NOT NULL,
138 workspace_id INTEGER NOT NULL,
139 path BLOB, // <-- No longer "NOT NULL"
140 scroll_top_row INTEGER NOT NULL DEFAULT 0,
141 scroll_horizontal_offset REAL NOT NULL DEFAULT 0,
142 scroll_vertical_offset REAL NOT NULL DEFAULT 0,
143 contents TEXT, // New
144 language TEXT, // New
145 PRIMARY KEY(item_id, workspace_id),
146 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
147 ON DELETE CASCADE
148 ON UPDATE CASCADE
149 ) STRICT;
150
151 INSERT INTO new_editors_tmp(item_id, workspace_id, path, scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset)
152 SELECT item_id, workspace_id, path, scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset
153 FROM editors;
154
155 DROP TABLE editors;
156
157 ALTER TABLE new_editors_tmp RENAME TO editors;
158 ),
159 sql! (
160 ALTER TABLE editors ADD COLUMN mtime_seconds INTEGER DEFAULT NULL;
161 ALTER TABLE editors ADD COLUMN mtime_nanos INTEGER DEFAULT NULL;
162 ),
163 sql! (
164 CREATE TABLE editor_selections (
165 item_id INTEGER NOT NULL,
166 editor_id INTEGER NOT NULL,
167 workspace_id INTEGER NOT NULL,
168 start INTEGER NOT NULL,
169 end INTEGER NOT NULL,
170 PRIMARY KEY(item_id),
171 FOREIGN KEY(editor_id, workspace_id) REFERENCES editors(item_id, workspace_id)
172 ON DELETE CASCADE
173 ) STRICT;
174 ),
175 sql! (
176 ALTER TABLE editors ADD COLUMN buffer_path TEXT;
177 UPDATE editors SET buffer_path = CAST(path AS TEXT);
178 ),
179 sql! (
180 CREATE TABLE editor_folds (
181 item_id INTEGER NOT NULL,
182 editor_id INTEGER NOT NULL,
183 workspace_id INTEGER NOT NULL,
184 start INTEGER NOT NULL,
185 end INTEGER NOT NULL,
186 PRIMARY KEY(item_id),
187 FOREIGN KEY(editor_id, workspace_id) REFERENCES editors(item_id, workspace_id)
188 ON DELETE CASCADE
189 ) STRICT;
190 ),
191 ];
192);
193
194// https://www.sqlite.org/limits.html
195// > <..> the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER,
196// > which defaults to <..> 32766 for SQLite versions after 3.32.0.
197const MAX_QUERY_PLACEHOLDERS: usize = 32000;
198
199impl EditorDb {
200 query! {
201 pub fn get_serialized_editor(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<SerializedEditor>> {
202 SELECT path, buffer_path, contents, language, mtime_seconds, mtime_nanos FROM editors
203 WHERE item_id = ? AND workspace_id = ?
204 }
205 }
206
207 query! {
208 pub async fn save_serialized_editor(item_id: ItemId, workspace_id: WorkspaceId, serialized_editor: SerializedEditor) -> Result<()> {
209 INSERT INTO editors
210 (item_id, workspace_id, path, buffer_path, contents, language, mtime_seconds, mtime_nanos)
211 VALUES
212 (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
213 ON CONFLICT DO UPDATE SET
214 item_id = ?1,
215 workspace_id = ?2,
216 path = ?3,
217 buffer_path = ?4,
218 contents = ?5,
219 language = ?6,
220 mtime_seconds = ?7,
221 mtime_nanos = ?8
222 }
223 }
224
225 // Returns the scroll top row, and offset
226 query! {
227 pub fn get_scroll_position(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<(u32, f32, f32)>> {
228 SELECT scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset
229 FROM editors
230 WHERE item_id = ? AND workspace_id = ?
231 }
232 }
233
234 query! {
235 pub async fn save_scroll_position(
236 item_id: ItemId,
237 workspace_id: WorkspaceId,
238 top_row: u32,
239 vertical_offset: f32,
240 horizontal_offset: f32
241 ) -> Result<()> {
242 UPDATE OR IGNORE editors
243 SET
244 scroll_top_row = ?3,
245 scroll_horizontal_offset = ?4,
246 scroll_vertical_offset = ?5
247 WHERE item_id = ?1 AND workspace_id = ?2
248 }
249 }
250
251 query! {
252 pub fn get_editor_selections(
253 editor_id: ItemId,
254 workspace_id: WorkspaceId
255 ) -> Result<Vec<(usize, usize)>> {
256 SELECT start, end
257 FROM editor_selections
258 WHERE editor_id = ?1 AND workspace_id = ?2
259 }
260 }
261
262 query! {
263 pub fn get_editor_folds(
264 editor_id: ItemId,
265 workspace_id: WorkspaceId
266 ) -> Result<Vec<(usize, usize)>> {
267 SELECT start, end
268 FROM editor_folds
269 WHERE editor_id = ?1 AND workspace_id = ?2
270 }
271 }
272
273 pub async fn save_editor_selections(
274 &self,
275 editor_id: ItemId,
276 workspace_id: WorkspaceId,
277 selections: Vec<(usize, usize)>,
278 ) -> Result<()> {
279 let mut first_selection;
280 let mut last_selection = 0_usize;
281 for (count, placeholders) in std::iter::once("(?1, ?2, ?, ?)")
282 .cycle()
283 .take(selections.len())
284 .chunks(MAX_QUERY_PLACEHOLDERS / 4)
285 .into_iter()
286 .map(|chunk| {
287 let mut count = 0;
288 let placeholders = chunk
289 .inspect(|_| {
290 count += 1;
291 })
292 .join(", ");
293 (count, placeholders)
294 })
295 .collect::<Vec<_>>()
296 {
297 first_selection = last_selection;
298 last_selection = last_selection + count;
299 let query = format!(
300 r#"
301DELETE FROM editor_selections WHERE editor_id = ?1 AND workspace_id = ?2;
302
303INSERT OR IGNORE INTO editor_selections (editor_id, workspace_id, start, end)
304VALUES {placeholders};
305"#
306 );
307
308 let selections = selections[first_selection..last_selection].to_vec();
309 self.write(move |conn| {
310 let mut statement = Statement::prepare(conn, query)?;
311 statement.bind(&editor_id, 1)?;
312 let mut next_index = statement.bind(&workspace_id, 2)?;
313 for (start, end) in selections {
314 next_index = statement.bind(&start, next_index)?;
315 next_index = statement.bind(&end, next_index)?;
316 }
317 statement.exec()
318 })
319 .await?;
320 }
321 Ok(())
322 }
323
324 pub async fn save_editor_folds(
325 &self,
326 editor_id: ItemId,
327 workspace_id: WorkspaceId,
328 folds: Vec<(usize, usize)>,
329 ) -> Result<()> {
330 let mut first_fold;
331 let mut last_fold = 0_usize;
332 for (count, placeholders) in std::iter::once("(?1, ?2, ?, ?)")
333 .cycle()
334 .take(folds.len())
335 .chunks(MAX_QUERY_PLACEHOLDERS / 4)
336 .into_iter()
337 .map(|chunk| {
338 let mut count = 0;
339 let placeholders = chunk
340 .inspect(|_| {
341 count += 1;
342 })
343 .join(", ");
344 (count, placeholders)
345 })
346 .collect::<Vec<_>>()
347 {
348 first_fold = last_fold;
349 last_fold = last_fold + count;
350 let query = format!(
351 r#"
352DELETE FROM editor_folds WHERE editor_id = ?1 AND workspace_id = ?2;
353
354INSERT OR IGNORE INTO editor_folds (editor_id, workspace_id, start, end)
355VALUES {placeholders};
356"#
357 );
358
359 let folds = folds[first_fold..last_fold].to_vec();
360 self.write(move |conn| {
361 let mut statement = Statement::prepare(conn, query)?;
362 statement.bind(&editor_id, 1)?;
363 let mut next_index = statement.bind(&workspace_id, 2)?;
364 for (start, end) in folds {
365 next_index = statement.bind(&start, next_index)?;
366 next_index = statement.bind(&end, next_index)?;
367 }
368 statement.exec()
369 })
370 .await?;
371 }
372 Ok(())
373 }
374
375 pub async fn delete_unloaded_items(
376 &self,
377 workspace: WorkspaceId,
378 alive_items: Vec<ItemId>,
379 ) -> Result<()> {
380 let placeholders = alive_items
381 .iter()
382 .map(|_| "?")
383 .collect::<Vec<&str>>()
384 .join(", ");
385
386 let query = format!(
387 "DELETE FROM editors WHERE workspace_id = ? AND item_id NOT IN ({placeholders})"
388 );
389
390 self.write(move |conn| {
391 let mut statement = Statement::prepare(conn, query)?;
392 let mut next_index = statement.bind(&workspace, 1)?;
393 for id in alive_items {
394 next_index = statement.bind(&id, next_index)?;
395 }
396 statement.exec()
397 })
398 .await
399 }
400}
401
402#[cfg(test)]
403mod tests {
404 use super::*;
405
406 #[gpui::test]
407 async fn test_save_and_get_serialized_editor() {
408 let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
409
410 let serialized_editor = SerializedEditor {
411 abs_path: Some(PathBuf::from("testing.txt")),
412 contents: None,
413 language: None,
414 mtime: None,
415 };
416
417 DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
418 .await
419 .unwrap();
420
421 let have = DB
422 .get_serialized_editor(1234, workspace_id)
423 .unwrap()
424 .unwrap();
425 assert_eq!(have, serialized_editor);
426
427 // Now update contents and language
428 let serialized_editor = SerializedEditor {
429 abs_path: Some(PathBuf::from("testing.txt")),
430 contents: Some("Test".to_owned()),
431 language: Some("Go".to_owned()),
432 mtime: None,
433 };
434
435 DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
436 .await
437 .unwrap();
438
439 let have = DB
440 .get_serialized_editor(1234, workspace_id)
441 .unwrap()
442 .unwrap();
443 assert_eq!(have, serialized_editor);
444
445 // Now set all the fields to NULL
446 let serialized_editor = SerializedEditor {
447 abs_path: None,
448 contents: None,
449 language: None,
450 mtime: None,
451 };
452
453 DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
454 .await
455 .unwrap();
456
457 let have = DB
458 .get_serialized_editor(1234, workspace_id)
459 .unwrap()
460 .unwrap();
461 assert_eq!(have, serialized_editor);
462
463 // Storing and retrieving mtime
464 let serialized_editor = SerializedEditor {
465 abs_path: None,
466 contents: None,
467 language: None,
468 mtime: Some(MTime::from_seconds_and_nanos(100, 42)),
469 };
470
471 DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
472 .await
473 .unwrap();
474
475 let have = DB
476 .get_serialized_editor(1234, workspace_id)
477 .unwrap()
478 .unwrap();
479 assert_eq!(have, serialized_editor);
480 }
481}