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