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 use gpui;
224
225 #[gpui::test]
226 async fn test_save_and_get_serialized_editor() {
227 let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap();
228
229 let serialized_editor = SerializedEditor {
230 path: Some(PathBuf::from("testing.txt")),
231 contents: None,
232 language: None,
233 mtime: None,
234 };
235
236 DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
237 .await
238 .unwrap();
239
240 let have = DB
241 .get_serialized_editor(1234, workspace_id)
242 .unwrap()
243 .unwrap();
244 assert_eq!(have, serialized_editor);
245
246 // Now update contents and language
247 let serialized_editor = SerializedEditor {
248 path: Some(PathBuf::from("testing.txt")),
249 contents: Some("Test".to_owned()),
250 language: Some("Go".to_owned()),
251 mtime: None,
252 };
253
254 DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
255 .await
256 .unwrap();
257
258 let have = DB
259 .get_serialized_editor(1234, workspace_id)
260 .unwrap()
261 .unwrap();
262 assert_eq!(have, serialized_editor);
263
264 // Now set all the fields to NULL
265 let serialized_editor = SerializedEditor {
266 path: None,
267 contents: None,
268 language: None,
269 mtime: None,
270 };
271
272 DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
273 .await
274 .unwrap();
275
276 let have = DB
277 .get_serialized_editor(1234, workspace_id)
278 .unwrap()
279 .unwrap();
280 assert_eq!(have, serialized_editor);
281
282 // Storing and retrieving mtime
283 let now = SystemTime::now();
284 let serialized_editor = SerializedEditor {
285 path: None,
286 contents: None,
287 language: None,
288 mtime: Some(now),
289 };
290
291 DB.save_serialized_editor(1234, workspace_id, serialized_editor.clone())
292 .await
293 .unwrap();
294
295 let have = DB
296 .get_serialized_editor(1234, workspace_id)
297 .unwrap()
298 .unwrap();
299 assert_eq!(have, serialized_editor);
300 }
301}