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 pub static ref DB: EditorDb<WorkspaceDb> = &[
101 sql! (
102 CREATE TABLE editors(
103 item_id INTEGER NOT NULL,
104 workspace_id INTEGER NOT NULL,
105 path BLOB NOT NULL,
106 PRIMARY KEY(item_id, workspace_id),
107 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
108 ON DELETE CASCADE
109 ON UPDATE CASCADE
110 ) STRICT;
111 ),
112 sql! (
113 ALTER TABLE editors ADD COLUMN scroll_top_row INTEGER NOT NULL DEFAULT 0;
114 ALTER TABLE editors ADD COLUMN scroll_horizontal_offset REAL NOT NULL DEFAULT 0;
115 ALTER TABLE editors ADD COLUMN scroll_vertical_offset REAL NOT NULL DEFAULT 0;
116 ),
117 sql! (
118 // Since sqlite3 doesn't support ALTER COLUMN, we create a new
119 // table, move the data over, drop the old table, rename new table.
120 CREATE TABLE new_editors_tmp (
121 item_id INTEGER NOT NULL,
122 workspace_id INTEGER NOT NULL,
123 path BLOB, // <-- No longer "NOT NULL"
124 scroll_top_row INTEGER NOT NULL DEFAULT 0,
125 scroll_horizontal_offset REAL NOT NULL DEFAULT 0,
126 scroll_vertical_offset REAL NOT NULL DEFAULT 0,
127 contents TEXT, // New
128 language TEXT, // New
129 PRIMARY KEY(item_id, workspace_id),
130 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
131 ON DELETE CASCADE
132 ON UPDATE CASCADE
133 ) STRICT;
134
135 INSERT INTO new_editors_tmp(item_id, workspace_id, path, scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset)
136 SELECT item_id, workspace_id, path, scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset
137 FROM editors;
138
139 DROP TABLE editors;
140
141 ALTER TABLE new_editors_tmp RENAME TO editors;
142 ),
143 sql! (
144 ALTER TABLE editors ADD COLUMN mtime_seconds INTEGER DEFAULT NULL;
145 ALTER TABLE editors ADD COLUMN mtime_nanos INTEGER DEFAULT NULL;
146 ),
147 sql! (
148 CREATE TABLE editor_selections (
149 item_id INTEGER NOT NULL,
150 editor_id INTEGER NOT NULL,
151 workspace_id INTEGER NOT NULL,
152 start INTEGER NOT NULL,
153 end INTEGER NOT NULL,
154 PRIMARY KEY(item_id),
155 FOREIGN KEY(editor_id, workspace_id) REFERENCES editors(item_id, workspace_id)
156 ON DELETE CASCADE
157 ) STRICT;
158 ),
159 sql! (
160 ALTER TABLE editors ADD COLUMN buffer_path TEXT;
161 UPDATE editors SET buffer_path = CAST(path AS TEXT);
162 ),
163 ];
164);
165
166// https://www.sqlite.org/limits.html
167// > <..> the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER,
168// > which defaults to <..> 32766 for SQLite versions after 3.32.0.
169const MAX_QUERY_PLACEHOLDERS: usize = 32000;
170
171impl EditorDb {
172 query! {
173 pub fn get_serialized_editor(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<SerializedEditor>> {
174 SELECT path, buffer_path, contents, language, mtime_seconds, mtime_nanos FROM editors
175 WHERE item_id = ? AND workspace_id = ?
176 }
177 }
178
179 query! {
180 pub async fn save_serialized_editor(item_id: ItemId, workspace_id: WorkspaceId, serialized_editor: SerializedEditor) -> Result<()> {
181 INSERT INTO editors
182 (item_id, workspace_id, path, buffer_path, contents, language, mtime_seconds, mtime_nanos)
183 VALUES
184 (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
185 ON CONFLICT DO UPDATE SET
186 item_id = ?1,
187 workspace_id = ?2,
188 path = ?3,
189 buffer_path = ?4,
190 contents = ?5,
191 language = ?6,
192 mtime_seconds = ?7,
193 mtime_nanos = ?8
194 }
195 }
196
197 // Returns the scroll top row, and offset
198 query! {
199 pub fn get_scroll_position(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<(u32, f32, f32)>> {
200 SELECT scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset
201 FROM editors
202 WHERE item_id = ? AND workspace_id = ?
203 }
204 }
205
206 query! {
207 pub async fn save_scroll_position(
208 item_id: ItemId,
209 workspace_id: WorkspaceId,
210 top_row: u32,
211 vertical_offset: f32,
212 horizontal_offset: f32
213 ) -> Result<()> {
214 UPDATE OR IGNORE editors
215 SET
216 scroll_top_row = ?3,
217 scroll_horizontal_offset = ?4,
218 scroll_vertical_offset = ?5
219 WHERE item_id = ?1 AND workspace_id = ?2
220 }
221 }
222
223 query! {
224 pub fn get_editor_selections(
225 editor_id: ItemId,
226 workspace_id: WorkspaceId
227 ) -> Result<Vec<(usize, usize)>> {
228 SELECT start, end
229 FROM editor_selections
230 WHERE editor_id = ?1 AND workspace_id = ?2
231 }
232 }
233
234 pub async fn save_editor_selections(
235 &self,
236 editor_id: ItemId,
237 workspace_id: WorkspaceId,
238 selections: Vec<(usize, usize)>,
239 ) -> Result<()> {
240 let mut first_selection;
241 let mut last_selection = 0_usize;
242 for (count, placeholders) in std::iter::once("(?1, ?2, ?, ?)")
243 .cycle()
244 .take(selections.len())
245 .chunks(MAX_QUERY_PLACEHOLDERS / 4)
246 .into_iter()
247 .map(|chunk| {
248 let mut count = 0;
249 let placeholders = chunk
250 .inspect(|_| {
251 count += 1;
252 })
253 .join(", ");
254 (count, placeholders)
255 })
256 .collect::<Vec<_>>()
257 {
258 first_selection = last_selection;
259 last_selection = last_selection + count;
260 let query = format!(
261 r#"
262DELETE FROM editor_selections WHERE editor_id = ?1 AND workspace_id = ?2;
263
264INSERT OR IGNORE INTO editor_selections (editor_id, workspace_id, start, end)
265VALUES {placeholders};
266"#
267 );
268
269 let selections = selections[first_selection..last_selection].to_vec();
270 self.write(move |conn| {
271 let mut statement = Statement::prepare(conn, query)?;
272 statement.bind(&editor_id, 1)?;
273 let mut next_index = statement.bind(&workspace_id, 2)?;
274 for (start, end) in selections {
275 next_index = statement.bind(&start, next_index)?;
276 next_index = statement.bind(&end, next_index)?;
277 }
278 statement.exec()
279 })
280 .await?;
281 }
282 Ok(())
283 }
284
285 pub async fn delete_unloaded_items(
286 &self,
287 workspace: WorkspaceId,
288 alive_items: Vec<ItemId>,
289 ) -> Result<()> {
290 let placeholders = alive_items
291 .iter()
292 .map(|_| "?")
293 .collect::<Vec<&str>>()
294 .join(", ");
295
296 let query = format!(
297 "DELETE FROM editors WHERE workspace_id = ? AND item_id NOT IN ({placeholders})"
298 );
299
300 self.write(move |conn| {
301 let mut statement = Statement::prepare(conn, query)?;
302 let mut next_index = statement.bind(&workspace, 1)?;
303 for id in alive_items {
304 next_index = statement.bind(&id, next_index)?;
305 }
306 statement.exec()
307 })
308 .await
309 }
310}
311
312#[cfg(test)]
313mod tests {
314 use super::*;
315
316 #[gpui::test]
317 async fn test_save_and_get_serialized_editor() {
318 let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
319
320 let serialized_editor = SerializedEditor {
321 abs_path: Some(PathBuf::from("testing.txt")),
322 contents: None,
323 language: None,
324 mtime: None,
325 };
326
327 DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
328 .await
329 .unwrap();
330
331 let have = DB
332 .get_serialized_editor(1234, workspace_id)
333 .unwrap()
334 .unwrap();
335 assert_eq!(have, serialized_editor);
336
337 // Now update contents and language
338 let serialized_editor = SerializedEditor {
339 abs_path: Some(PathBuf::from("testing.txt")),
340 contents: Some("Test".to_owned()),
341 language: Some("Go".to_owned()),
342 mtime: None,
343 };
344
345 DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
346 .await
347 .unwrap();
348
349 let have = DB
350 .get_serialized_editor(1234, workspace_id)
351 .unwrap()
352 .unwrap();
353 assert_eq!(have, serialized_editor);
354
355 // Now set all the fields to NULL
356 let serialized_editor = SerializedEditor {
357 abs_path: None,
358 contents: None,
359 language: None,
360 mtime: None,
361 };
362
363 DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
364 .await
365 .unwrap();
366
367 let have = DB
368 .get_serialized_editor(1234, workspace_id)
369 .unwrap()
370 .unwrap();
371 assert_eq!(have, serialized_editor);
372
373 // Storing and retrieving mtime
374 let serialized_editor = SerializedEditor {
375 abs_path: None,
376 contents: None,
377 language: None,
378 mtime: Some(MTime::from_seconds_and_nanos(100, 42)),
379 };
380
381 DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
382 .await
383 .unwrap();
384
385 let have = DB
386 .get_serialized_editor(1234, workspace_id)
387 .unwrap()
388 .unwrap();
389 assert_eq!(have, serialized_editor);
390 }
391}