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 log::debug!("Saving selections for editor {editor_id} in workspace {workspace_id:?}");
280 let mut first_selection;
281 let mut last_selection = 0_usize;
282 for (count, placeholders) in std::iter::once("(?1, ?2, ?, ?)")
283 .cycle()
284 .take(selections.len())
285 .chunks(MAX_QUERY_PLACEHOLDERS / 4)
286 .into_iter()
287 .map(|chunk| {
288 let mut count = 0;
289 let placeholders = chunk
290 .inspect(|_| {
291 count += 1;
292 })
293 .join(", ");
294 (count, placeholders)
295 })
296 .collect::<Vec<_>>()
297 {
298 first_selection = last_selection;
299 last_selection = last_selection + count;
300 let query = format!(
301 r#"
302DELETE FROM editor_selections WHERE editor_id = ?1 AND workspace_id = ?2;
303
304INSERT OR IGNORE INTO editor_selections (editor_id, workspace_id, start, end)
305VALUES {placeholders};
306"#
307 );
308
309 let selections = selections[first_selection..last_selection].to_vec();
310 self.write(move |conn| {
311 let mut statement = Statement::prepare(conn, query)?;
312 statement.bind(&editor_id, 1)?;
313 let mut next_index = statement.bind(&workspace_id, 2)?;
314 for (start, end) in selections {
315 next_index = statement.bind(&start, next_index)?;
316 next_index = statement.bind(&end, next_index)?;
317 }
318 statement.exec()
319 })
320 .await?;
321 }
322 Ok(())
323 }
324
325 pub async fn save_editor_folds(
326 &self,
327 editor_id: ItemId,
328 workspace_id: WorkspaceId,
329 folds: Vec<(usize, usize)>,
330 ) -> Result<()> {
331 log::debug!("Saving folds for editor {editor_id} in workspace {workspace_id:?}");
332 let mut first_fold;
333 let mut last_fold = 0_usize;
334 for (count, placeholders) in std::iter::once("(?1, ?2, ?, ?)")
335 .cycle()
336 .take(folds.len())
337 .chunks(MAX_QUERY_PLACEHOLDERS / 4)
338 .into_iter()
339 .map(|chunk| {
340 let mut count = 0;
341 let placeholders = chunk
342 .inspect(|_| {
343 count += 1;
344 })
345 .join(", ");
346 (count, placeholders)
347 })
348 .collect::<Vec<_>>()
349 {
350 first_fold = last_fold;
351 last_fold = last_fold + count;
352 let query = format!(
353 r#"
354DELETE FROM editor_folds WHERE editor_id = ?1 AND workspace_id = ?2;
355
356INSERT OR IGNORE INTO editor_folds (editor_id, workspace_id, start, end)
357VALUES {placeholders};
358"#
359 );
360
361 let folds = folds[first_fold..last_fold].to_vec();
362 self.write(move |conn| {
363 let mut statement = Statement::prepare(conn, query)?;
364 statement.bind(&editor_id, 1)?;
365 let mut next_index = statement.bind(&workspace_id, 2)?;
366 for (start, end) in folds {
367 next_index = statement.bind(&start, next_index)?;
368 next_index = statement.bind(&end, next_index)?;
369 }
370 statement.exec()
371 })
372 .await?;
373 }
374 Ok(())
375 }
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381
382 #[gpui::test]
383 async fn test_save_and_get_serialized_editor() {
384 let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
385
386 let serialized_editor = SerializedEditor {
387 abs_path: Some(PathBuf::from("testing.txt")),
388 contents: None,
389 language: None,
390 mtime: None,
391 };
392
393 DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
394 .await
395 .unwrap();
396
397 let have = DB
398 .get_serialized_editor(1234, workspace_id)
399 .unwrap()
400 .unwrap();
401 assert_eq!(have, serialized_editor);
402
403 // Now update contents and language
404 let serialized_editor = SerializedEditor {
405 abs_path: Some(PathBuf::from("testing.txt")),
406 contents: Some("Test".to_owned()),
407 language: Some("Go".to_owned()),
408 mtime: None,
409 };
410
411 DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
412 .await
413 .unwrap();
414
415 let have = DB
416 .get_serialized_editor(1234, workspace_id)
417 .unwrap()
418 .unwrap();
419 assert_eq!(have, serialized_editor);
420
421 // Now set all the fields to NULL
422 let serialized_editor = SerializedEditor {
423 abs_path: None,
424 contents: None,
425 language: None,
426 mtime: None,
427 };
428
429 DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
430 .await
431 .unwrap();
432
433 let have = DB
434 .get_serialized_editor(1234, workspace_id)
435 .unwrap()
436 .unwrap();
437 assert_eq!(have, serialized_editor);
438
439 // Storing and retrieving mtime
440 let serialized_editor = SerializedEditor {
441 abs_path: None,
442 contents: None,
443 language: None,
444 mtime: Some(MTime::from_seconds_and_nanos(100, 42)),
445 };
446
447 DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
448 .await
449 .unwrap();
450
451 let have = DB
452 .get_serialized_editor(1234, workspace_id)
453 .unwrap()
454 .unwrap();
455 assert_eq!(have, serialized_editor);
456 }
457}