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