1use anyhow::Result;
2use db::sqlez::bindable::{Bind, Column, StaticColumnCount};
3use db::sqlez::statement::Statement;
4use fs::MTime;
5use std::path::PathBuf;
6
7use db::sqlez_macros::sql;
8use db::{define_connection, query};
9
10use workspace::{ItemId, WorkspaceDb, WorkspaceId};
11
12#[derive(Clone, Debug, PartialEq, Default)]
13pub(crate) struct SerializedEditor {
14 pub(crate) abs_path: Option<PathBuf>,
15 pub(crate) contents: Option<String>,
16 pub(crate) language: Option<String>,
17 pub(crate) mtime: Option<MTime>,
18}
19
20impl StaticColumnCount for SerializedEditor {
21 fn column_count() -> usize {
22 5
23 }
24}
25
26impl Bind for SerializedEditor {
27 fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
28 let start_index = statement.bind(&self.abs_path, start_index)?;
29 let start_index = statement.bind(&self.contents, start_index)?;
30 let start_index = statement.bind(&self.language, start_index)?;
31
32 let start_index = match self
33 .mtime
34 .and_then(|mtime| mtime.to_seconds_and_nanos_for_persistence())
35 {
36 Some((seconds, nanos)) => {
37 let start_index = statement.bind(&(seconds as i64), start_index)?;
38 statement.bind(&(nanos as i32), start_index)?
39 }
40 None => {
41 let start_index = statement.bind::<Option<i64>>(&None, start_index)?;
42 statement.bind::<Option<i32>>(&None, start_index)?
43 }
44 };
45 Ok(start_index)
46 }
47}
48
49impl Column for SerializedEditor {
50 fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
51 let (abs_path, start_index): (Option<PathBuf>, i32) =
52 Column::column(statement, start_index)?;
53 let (contents, start_index): (Option<String>, i32) =
54 Column::column(statement, start_index)?;
55 let (language, start_index): (Option<String>, i32) =
56 Column::column(statement, start_index)?;
57 let (mtime_seconds, start_index): (Option<i64>, i32) =
58 Column::column(statement, start_index)?;
59 let (mtime_nanos, start_index): (Option<i32>, i32) =
60 Column::column(statement, start_index)?;
61
62 let mtime = mtime_seconds
63 .zip(mtime_nanos)
64 .map(|(seconds, nanos)| MTime::from_seconds_and_nanos(seconds as u64, nanos as u32));
65
66 let editor = Self {
67 abs_path,
68 contents,
69 language,
70 mtime,
71 };
72 Ok((editor, start_index))
73 }
74}
75
76define_connection!(
77 // Current schema shape using pseudo-rust syntax:
78 // editors(
79 // item_id: usize,
80 // workspace_id: usize,
81 // path: Option<PathBuf>,
82 // scroll_top_row: usize,
83 // scroll_vertical_offset: f32,
84 // scroll_horizontal_offset: f32,
85 // content: Option<String>,
86 // language: Option<String>,
87 // mtime_seconds: Option<i64>,
88 // mtime_nanos: Option<i32>,
89 // )
90 pub static ref DB: EditorDb<WorkspaceDb> = &[
91 sql! (
92 CREATE TABLE editors(
93 item_id INTEGER NOT NULL,
94 workspace_id INTEGER NOT NULL,
95 path BLOB NOT NULL,
96 PRIMARY KEY(item_id, workspace_id),
97 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
98 ON DELETE CASCADE
99 ON UPDATE CASCADE
100 ) STRICT;
101 ),
102 sql! (
103 ALTER TABLE editors ADD COLUMN scroll_top_row INTEGER NOT NULL DEFAULT 0;
104 ALTER TABLE editors ADD COLUMN scroll_horizontal_offset REAL NOT NULL DEFAULT 0;
105 ALTER TABLE editors ADD COLUMN scroll_vertical_offset REAL NOT NULL DEFAULT 0;
106 ),
107 sql! (
108 // Since sqlite3 doesn't support ALTER COLUMN, we create a new
109 // table, move the data over, drop the old table, rename new table.
110 CREATE TABLE new_editors_tmp (
111 item_id INTEGER NOT NULL,
112 workspace_id INTEGER NOT NULL,
113 path BLOB, // <-- No longer "NOT NULL"
114 scroll_top_row INTEGER NOT NULL DEFAULT 0,
115 scroll_horizontal_offset REAL NOT NULL DEFAULT 0,
116 scroll_vertical_offset REAL NOT NULL DEFAULT 0,
117 contents TEXT, // New
118 language TEXT, // New
119 PRIMARY KEY(item_id, workspace_id),
120 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
121 ON DELETE CASCADE
122 ON UPDATE CASCADE
123 ) STRICT;
124
125 INSERT INTO new_editors_tmp(item_id, workspace_id, path, scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset)
126 SELECT item_id, workspace_id, path, scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset
127 FROM editors;
128
129 DROP TABLE editors;
130
131 ALTER TABLE new_editors_tmp RENAME TO editors;
132 ),
133 sql! (
134 ALTER TABLE editors ADD COLUMN mtime_seconds INTEGER DEFAULT NULL;
135 ALTER TABLE editors ADD COLUMN mtime_nanos INTEGER DEFAULT NULL;
136 ),
137 ];
138);
139
140impl EditorDb {
141 query! {
142 pub fn get_serialized_editor(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<SerializedEditor>> {
143 SELECT path, contents, language, mtime_seconds, mtime_nanos FROM editors
144 WHERE item_id = ? AND workspace_id = ?
145 }
146 }
147
148 query! {
149 pub async fn save_serialized_editor(item_id: ItemId, workspace_id: WorkspaceId, serialized_editor: SerializedEditor) -> Result<()> {
150 INSERT INTO editors
151 (item_id, workspace_id, path, contents, language, mtime_seconds, mtime_nanos)
152 VALUES
153 (?1, ?2, ?3, ?4, ?5, ?6, ?7)
154 ON CONFLICT DO UPDATE SET
155 item_id = ?1,
156 workspace_id = ?2,
157 path = ?3,
158 contents = ?4,
159 language = ?5,
160 mtime_seconds = ?6,
161 mtime_nanos = ?7
162 }
163 }
164
165 // Returns the scroll top row, and offset
166 query! {
167 pub fn get_scroll_position(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<(u32, f32, f32)>> {
168 SELECT scroll_top_row, scroll_horizontal_offset, scroll_vertical_offset
169 FROM editors
170 WHERE item_id = ? AND workspace_id = ?
171 }
172 }
173
174 query! {
175 pub async fn save_scroll_position(
176 item_id: ItemId,
177 workspace_id: WorkspaceId,
178 top_row: u32,
179 vertical_offset: f32,
180 horizontal_offset: f32
181 ) -> Result<()> {
182 UPDATE OR IGNORE editors
183 SET
184 scroll_top_row = ?3,
185 scroll_horizontal_offset = ?4,
186 scroll_vertical_offset = ?5
187 WHERE item_id = ?1 AND workspace_id = ?2
188 }
189 }
190
191 pub async fn delete_unloaded_items(
192 &self,
193 workspace: WorkspaceId,
194 alive_items: Vec<ItemId>,
195 ) -> Result<()> {
196 let placeholders = alive_items
197 .iter()
198 .map(|_| "?")
199 .collect::<Vec<&str>>()
200 .join(", ");
201
202 let query = format!(
203 "DELETE FROM editors WHERE workspace_id = ? AND item_id NOT IN ({placeholders})"
204 );
205
206 self.write(move |conn| {
207 let mut statement = Statement::prepare(conn, query)?;
208 let mut next_index = statement.bind(&workspace, 1)?;
209 for id in alive_items {
210 next_index = statement.bind(&id, next_index)?;
211 }
212 statement.exec()
213 })
214 .await
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221
222 #[gpui::test]
223 async fn test_save_and_get_serialized_editor() {
224 let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
225
226 let serialized_editor = SerializedEditor {
227 abs_path: Some(PathBuf::from("testing.txt")),
228 contents: None,
229 language: None,
230 mtime: None,
231 };
232
233 DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
234 .await
235 .unwrap();
236
237 let have = DB
238 .get_serialized_editor(1234, workspace_id)
239 .unwrap()
240 .unwrap();
241 assert_eq!(have, serialized_editor);
242
243 // Now update contents and language
244 let serialized_editor = SerializedEditor {
245 abs_path: Some(PathBuf::from("testing.txt")),
246 contents: Some("Test".to_owned()),
247 language: Some("Go".to_owned()),
248 mtime: None,
249 };
250
251 DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
252 .await
253 .unwrap();
254
255 let have = DB
256 .get_serialized_editor(1234, workspace_id)
257 .unwrap()
258 .unwrap();
259 assert_eq!(have, serialized_editor);
260
261 // Now set all the fields to NULL
262 let serialized_editor = SerializedEditor {
263 abs_path: None,
264 contents: None,
265 language: None,
266 mtime: None,
267 };
268
269 DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
270 .await
271 .unwrap();
272
273 let have = DB
274 .get_serialized_editor(1234, workspace_id)
275 .unwrap()
276 .unwrap();
277 assert_eq!(have, serialized_editor);
278
279 // Storing and retrieving mtime
280 let serialized_editor = SerializedEditor {
281 abs_path: None,
282 contents: None,
283 language: None,
284 mtime: Some(MTime::from_seconds_and_nanos(100, 42)),
285 };
286
287 DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
288 .await
289 .unwrap();
290
291 let have = DB
292 .get_serialized_editor(1234, workspace_id)
293 .unwrap()
294 .unwrap();
295 assert_eq!(have, serialized_editor);
296 }
297}