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