1pub mod model;
2
3use std::path::Path;
4
5use anyhow::{anyhow, bail, Context, Result};
6use client::DevServerProjectId;
7use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql};
8use gpui::{point, size, Axis, Bounds, WindowBounds, WindowId};
9
10use remote::ssh_session::SshProjectId;
11use sqlez::{
12 bindable::{Bind, Column, StaticColumnCount},
13 statement::Statement,
14};
15
16use ui::px;
17use util::{maybe, ResultExt};
18use uuid::Uuid;
19
20use crate::WorkspaceId;
21
22use model::{
23 GroupId, LocalPaths, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup,
24 SerializedSshProject, SerializedWorkspace,
25};
26
27use self::model::{
28 DockStructure, LocalPathsOrder, SerializedDevServerProject, SerializedWorkspaceLocation,
29};
30
31#[derive(Copy, Clone, Debug, PartialEq)]
32pub(crate) struct SerializedAxis(pub(crate) gpui::Axis);
33impl sqlez::bindable::StaticColumnCount for SerializedAxis {}
34impl sqlez::bindable::Bind for SerializedAxis {
35 fn bind(
36 &self,
37 statement: &sqlez::statement::Statement,
38 start_index: i32,
39 ) -> anyhow::Result<i32> {
40 match self.0 {
41 gpui::Axis::Horizontal => "Horizontal",
42 gpui::Axis::Vertical => "Vertical",
43 }
44 .bind(statement, start_index)
45 }
46}
47
48impl sqlez::bindable::Column for SerializedAxis {
49 fn column(
50 statement: &mut sqlez::statement::Statement,
51 start_index: i32,
52 ) -> anyhow::Result<(Self, i32)> {
53 String::column(statement, start_index).and_then(|(axis_text, next_index)| {
54 Ok((
55 match axis_text.as_str() {
56 "Horizontal" => Self(Axis::Horizontal),
57 "Vertical" => Self(Axis::Vertical),
58 _ => anyhow::bail!("Stored serialized item kind is incorrect"),
59 },
60 next_index,
61 ))
62 })
63 }
64}
65
66#[derive(Copy, Clone, Debug, PartialEq, Default)]
67pub(crate) struct SerializedWindowBounds(pub(crate) WindowBounds);
68
69impl StaticColumnCount for SerializedWindowBounds {
70 fn column_count() -> usize {
71 5
72 }
73}
74
75impl Bind for SerializedWindowBounds {
76 fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
77 match self.0 {
78 WindowBounds::Windowed(bounds) => {
79 let next_index = statement.bind(&"Windowed", start_index)?;
80 statement.bind(
81 &(
82 SerializedPixels(bounds.origin.x),
83 SerializedPixels(bounds.origin.y),
84 SerializedPixels(bounds.size.width),
85 SerializedPixels(bounds.size.height),
86 ),
87 next_index,
88 )
89 }
90 WindowBounds::Maximized(bounds) => {
91 let next_index = statement.bind(&"Maximized", start_index)?;
92 statement.bind(
93 &(
94 SerializedPixels(bounds.origin.x),
95 SerializedPixels(bounds.origin.y),
96 SerializedPixels(bounds.size.width),
97 SerializedPixels(bounds.size.height),
98 ),
99 next_index,
100 )
101 }
102 WindowBounds::Fullscreen(bounds) => {
103 let next_index = statement.bind(&"FullScreen", start_index)?;
104 statement.bind(
105 &(
106 SerializedPixels(bounds.origin.x),
107 SerializedPixels(bounds.origin.y),
108 SerializedPixels(bounds.size.width),
109 SerializedPixels(bounds.size.height),
110 ),
111 next_index,
112 )
113 }
114 }
115 }
116}
117
118impl Column for SerializedWindowBounds {
119 fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
120 let (window_state, next_index) = String::column(statement, start_index)?;
121 let ((x, y, width, height), _): ((i32, i32, i32, i32), _) =
122 Column::column(statement, next_index)?;
123 let bounds = Bounds {
124 origin: point(px(x as f32), px(y as f32)),
125 size: size(px(width as f32), px(height as f32)),
126 };
127
128 let status = match window_state.as_str() {
129 "Windowed" | "Fixed" => SerializedWindowBounds(WindowBounds::Windowed(bounds)),
130 "Maximized" => SerializedWindowBounds(WindowBounds::Maximized(bounds)),
131 "FullScreen" => SerializedWindowBounds(WindowBounds::Fullscreen(bounds)),
132 _ => bail!("Window State did not have a valid string"),
133 };
134
135 Ok((status, next_index + 4))
136 }
137}
138
139#[derive(Clone, Debug, PartialEq)]
140struct SerializedPixels(gpui::Pixels);
141impl sqlez::bindable::StaticColumnCount for SerializedPixels {}
142
143impl sqlez::bindable::Bind for SerializedPixels {
144 fn bind(
145 &self,
146 statement: &sqlez::statement::Statement,
147 start_index: i32,
148 ) -> anyhow::Result<i32> {
149 let this: i32 = self.0 .0 as i32;
150 this.bind(statement, start_index)
151 }
152}
153
154define_connection! {
155 // Current schema shape using pseudo-rust syntax:
156 //
157 // workspaces(
158 // workspace_id: usize, // Primary key for workspaces
159 // local_paths: Bincode<Vec<PathBuf>>,
160 // local_paths_order: Bincode<Vec<usize>>,
161 // dock_visible: bool, // Deprecated
162 // dock_anchor: DockAnchor, // Deprecated
163 // dock_pane: Option<usize>, // Deprecated
164 // left_sidebar_open: boolean,
165 // timestamp: String, // UTC YYYY-MM-DD HH:MM:SS
166 // window_state: String, // WindowBounds Discriminant
167 // window_x: Option<f32>, // WindowBounds::Fixed RectF x
168 // window_y: Option<f32>, // WindowBounds::Fixed RectF y
169 // window_width: Option<f32>, // WindowBounds::Fixed RectF width
170 // window_height: Option<f32>, // WindowBounds::Fixed RectF height
171 // display: Option<Uuid>, // Display id
172 // fullscreen: Option<bool>, // Is the window fullscreen?
173 // centered_layout: Option<bool>, // Is the Centered Layout mode activated?
174 // session_id: Option<String>, // Session id
175 // window_id: Option<u64>, // Window Id
176 // )
177 //
178 // pane_groups(
179 // group_id: usize, // Primary key for pane_groups
180 // workspace_id: usize, // References workspaces table
181 // parent_group_id: Option<usize>, // None indicates that this is the root node
182 // position: Optiopn<usize>, // None indicates that this is the root node
183 // axis: Option<Axis>, // 'Vertical', 'Horizontal'
184 // flexes: Option<Vec<f32>>, // A JSON array of floats
185 // )
186 //
187 // panes(
188 // pane_id: usize, // Primary key for panes
189 // workspace_id: usize, // References workspaces table
190 // active: bool,
191 // )
192 //
193 // center_panes(
194 // pane_id: usize, // Primary key for center_panes
195 // parent_group_id: Option<usize>, // References pane_groups. If none, this is the root
196 // position: Option<usize>, // None indicates this is the root
197 // )
198 //
199 // CREATE TABLE items(
200 // item_id: usize, // This is the item's view id, so this is not unique
201 // workspace_id: usize, // References workspaces table
202 // pane_id: usize, // References panes table
203 // kind: String, // Indicates which view this connects to. This is the key in the item_deserializers global
204 // position: usize, // Position of the item in the parent pane. This is equivalent to panes' position column
205 // active: bool, // Indicates if this item is the active one in the pane
206 // preview: bool // Indicates if this item is a preview item
207 // )
208 pub static ref DB: WorkspaceDb<()> =
209 &[sql!(
210 CREATE TABLE workspaces(
211 workspace_id INTEGER PRIMARY KEY,
212 workspace_location BLOB UNIQUE,
213 dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
214 dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
215 dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed.
216 left_sidebar_open INTEGER, // Boolean
217 timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
218 FOREIGN KEY(dock_pane) REFERENCES panes(pane_id)
219 ) STRICT;
220
221 CREATE TABLE pane_groups(
222 group_id INTEGER PRIMARY KEY,
223 workspace_id INTEGER NOT NULL,
224 parent_group_id INTEGER, // NULL indicates that this is a root node
225 position INTEGER, // NULL indicates that this is a root node
226 axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal'
227 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
228 ON DELETE CASCADE
229 ON UPDATE CASCADE,
230 FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
231 ) STRICT;
232
233 CREATE TABLE panes(
234 pane_id INTEGER PRIMARY KEY,
235 workspace_id INTEGER NOT NULL,
236 active INTEGER NOT NULL, // Boolean
237 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
238 ON DELETE CASCADE
239 ON UPDATE CASCADE
240 ) STRICT;
241
242 CREATE TABLE center_panes(
243 pane_id INTEGER PRIMARY KEY,
244 parent_group_id INTEGER, // NULL means that this is a root pane
245 position INTEGER, // NULL means that this is a root pane
246 FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
247 ON DELETE CASCADE,
248 FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
249 ) STRICT;
250
251 CREATE TABLE items(
252 item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique
253 workspace_id INTEGER NOT NULL,
254 pane_id INTEGER NOT NULL,
255 kind TEXT NOT NULL,
256 position INTEGER NOT NULL,
257 active INTEGER NOT NULL,
258 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
259 ON DELETE CASCADE
260 ON UPDATE CASCADE,
261 FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
262 ON DELETE CASCADE,
263 PRIMARY KEY(item_id, workspace_id)
264 ) STRICT;
265 ),
266 sql!(
267 ALTER TABLE workspaces ADD COLUMN window_state TEXT;
268 ALTER TABLE workspaces ADD COLUMN window_x REAL;
269 ALTER TABLE workspaces ADD COLUMN window_y REAL;
270 ALTER TABLE workspaces ADD COLUMN window_width REAL;
271 ALTER TABLE workspaces ADD COLUMN window_height REAL;
272 ALTER TABLE workspaces ADD COLUMN display BLOB;
273 ),
274 // Drop foreign key constraint from workspaces.dock_pane to panes table.
275 sql!(
276 CREATE TABLE workspaces_2(
277 workspace_id INTEGER PRIMARY KEY,
278 workspace_location BLOB UNIQUE,
279 dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
280 dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
281 dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed.
282 left_sidebar_open INTEGER, // Boolean
283 timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
284 window_state TEXT,
285 window_x REAL,
286 window_y REAL,
287 window_width REAL,
288 window_height REAL,
289 display BLOB
290 ) STRICT;
291 INSERT INTO workspaces_2 SELECT * FROM workspaces;
292 DROP TABLE workspaces;
293 ALTER TABLE workspaces_2 RENAME TO workspaces;
294 ),
295 // Add panels related information
296 sql!(
297 ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool
298 ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT;
299 ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool
300 ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT;
301 ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool
302 ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT;
303 ),
304 // Add panel zoom persistence
305 sql!(
306 ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool
307 ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool
308 ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool
309 ),
310 // Add pane group flex data
311 sql!(
312 ALTER TABLE pane_groups ADD COLUMN flexes TEXT;
313 ),
314 // Add fullscreen field to workspace
315 // Deprecated, `WindowBounds` holds the fullscreen state now.
316 // Preserving so users can downgrade Zed.
317 sql!(
318 ALTER TABLE workspaces ADD COLUMN fullscreen INTEGER; //bool
319 ),
320 // Add preview field to items
321 sql!(
322 ALTER TABLE items ADD COLUMN preview INTEGER; //bool
323 ),
324 // Add centered_layout field to workspace
325 sql!(
326 ALTER TABLE workspaces ADD COLUMN centered_layout INTEGER; //bool
327 ),
328 sql!(
329 CREATE TABLE remote_projects (
330 remote_project_id INTEGER NOT NULL UNIQUE,
331 path TEXT,
332 dev_server_name TEXT
333 );
334 ALTER TABLE workspaces ADD COLUMN remote_project_id INTEGER;
335 ALTER TABLE workspaces RENAME COLUMN workspace_location TO local_paths;
336 ),
337 sql!(
338 DROP TABLE remote_projects;
339 CREATE TABLE dev_server_projects (
340 id INTEGER NOT NULL UNIQUE,
341 path TEXT,
342 dev_server_name TEXT
343 );
344 ALTER TABLE workspaces DROP COLUMN remote_project_id;
345 ALTER TABLE workspaces ADD COLUMN dev_server_project_id INTEGER;
346 ),
347 sql!(
348 ALTER TABLE workspaces ADD COLUMN local_paths_order BLOB;
349 ),
350 sql!(
351 ALTER TABLE workspaces ADD COLUMN session_id TEXT DEFAULT NULL;
352 ),
353 sql!(
354 ALTER TABLE workspaces ADD COLUMN window_id INTEGER DEFAULT NULL;
355 ),
356 sql!(
357 ALTER TABLE panes ADD COLUMN pinned_count INTEGER DEFAULT 0;
358 ),
359 sql!(
360 CREATE TABLE ssh_projects (
361 id INTEGER PRIMARY KEY,
362 host TEXT NOT NULL,
363 port INTEGER,
364 path TEXT NOT NULL,
365 user TEXT
366 );
367 ALTER TABLE workspaces ADD COLUMN ssh_project_id INTEGER REFERENCES ssh_projects(id) ON DELETE CASCADE;
368 ),
369 ];
370}
371
372impl WorkspaceDb {
373 /// Returns a serialized workspace for the given worktree_roots. If the passed array
374 /// is empty, the most recent workspace is returned instead. If no workspace for the
375 /// passed roots is stored, returns none.
376 pub(crate) fn workspace_for_roots<P: AsRef<Path>>(
377 &self,
378 worktree_roots: &[P],
379 ) -> Option<SerializedWorkspace> {
380 let local_paths = LocalPaths::new(worktree_roots);
381
382 // Note that we re-assign the workspace_id here in case it's empty
383 // and we've grabbed the most recent workspace
384 let (
385 workspace_id,
386 local_paths,
387 local_paths_order,
388 window_bounds,
389 display,
390 centered_layout,
391 docks,
392 window_id,
393 ): (
394 WorkspaceId,
395 Option<LocalPaths>,
396 Option<LocalPathsOrder>,
397 Option<SerializedWindowBounds>,
398 Option<Uuid>,
399 Option<bool>,
400 DockStructure,
401 Option<u64>,
402 ) = self
403 .select_row_bound(sql! {
404 SELECT
405 workspace_id,
406 local_paths,
407 local_paths_order,
408 window_state,
409 window_x,
410 window_y,
411 window_width,
412 window_height,
413 display,
414 centered_layout,
415 left_dock_visible,
416 left_dock_active_panel,
417 left_dock_zoom,
418 right_dock_visible,
419 right_dock_active_panel,
420 right_dock_zoom,
421 bottom_dock_visible,
422 bottom_dock_active_panel,
423 bottom_dock_zoom,
424 window_id
425 FROM workspaces
426 WHERE local_paths = ?
427 })
428 .and_then(|mut prepared_statement| (prepared_statement)(&local_paths))
429 .context("No workspaces found")
430 .warn_on_err()
431 .flatten()?;
432
433 let local_paths = local_paths?;
434 let location = match local_paths_order {
435 Some(order) => SerializedWorkspaceLocation::Local(local_paths, order),
436 None => {
437 let order = LocalPathsOrder::default_for_paths(&local_paths);
438 SerializedWorkspaceLocation::Local(local_paths, order)
439 }
440 };
441
442 Some(SerializedWorkspace {
443 id: workspace_id,
444 location,
445 center_group: self
446 .get_center_pane_group(workspace_id)
447 .context("Getting center group")
448 .log_err()?,
449 window_bounds,
450 centered_layout: centered_layout.unwrap_or(false),
451 display,
452 docks,
453 session_id: None,
454 window_id,
455 })
456 }
457
458 pub(crate) fn workspace_for_dev_server_project(
459 &self,
460 dev_server_project_id: DevServerProjectId,
461 ) -> Option<SerializedWorkspace> {
462 // Note that we re-assign the workspace_id here in case it's empty
463 // and we've grabbed the most recent workspace
464 let (
465 workspace_id,
466 dev_server_project_id,
467 window_bounds,
468 display,
469 centered_layout,
470 docks,
471 window_id,
472 ): (
473 WorkspaceId,
474 Option<u64>,
475 Option<SerializedWindowBounds>,
476 Option<Uuid>,
477 Option<bool>,
478 DockStructure,
479 Option<u64>,
480 ) = self
481 .select_row_bound(sql! {
482 SELECT
483 workspace_id,
484 dev_server_project_id,
485 window_state,
486 window_x,
487 window_y,
488 window_width,
489 window_height,
490 display,
491 centered_layout,
492 left_dock_visible,
493 left_dock_active_panel,
494 left_dock_zoom,
495 right_dock_visible,
496 right_dock_active_panel,
497 right_dock_zoom,
498 bottom_dock_visible,
499 bottom_dock_active_panel,
500 bottom_dock_zoom,
501 window_id
502 FROM workspaces
503 WHERE dev_server_project_id = ?
504 })
505 .and_then(|mut prepared_statement| (prepared_statement)(dev_server_project_id.0))
506 .context("No workspaces found")
507 .warn_on_err()
508 .flatten()?;
509
510 let dev_server_project_id = dev_server_project_id?;
511
512 let dev_server_project: SerializedDevServerProject = self
513 .select_row_bound(sql! {
514 SELECT id, path, dev_server_name
515 FROM dev_server_projects
516 WHERE id = ?
517 })
518 .and_then(|mut prepared_statement| (prepared_statement)(dev_server_project_id))
519 .context("No remote project found")
520 .warn_on_err()
521 .flatten()?;
522
523 let location = SerializedWorkspaceLocation::DevServer(dev_server_project);
524
525 Some(SerializedWorkspace {
526 id: workspace_id,
527 location,
528 center_group: self
529 .get_center_pane_group(workspace_id)
530 .context("Getting center group")
531 .log_err()?,
532 window_bounds,
533 centered_layout: centered_layout.unwrap_or(false),
534 display,
535 docks,
536 session_id: None,
537 window_id,
538 })
539 }
540
541 pub(crate) fn workspace_for_ssh_project(
542 &self,
543 ssh_project: &SerializedSshProject,
544 ) -> Option<SerializedWorkspace> {
545 let (workspace_id, window_bounds, display, centered_layout, docks, window_id): (
546 WorkspaceId,
547 Option<SerializedWindowBounds>,
548 Option<Uuid>,
549 Option<bool>,
550 DockStructure,
551 Option<u64>,
552 ) = self
553 .select_row_bound(sql! {
554 SELECT
555 workspace_id,
556 window_state,
557 window_x,
558 window_y,
559 window_width,
560 window_height,
561 display,
562 centered_layout,
563 left_dock_visible,
564 left_dock_active_panel,
565 left_dock_zoom,
566 right_dock_visible,
567 right_dock_active_panel,
568 right_dock_zoom,
569 bottom_dock_visible,
570 bottom_dock_active_panel,
571 bottom_dock_zoom,
572 window_id
573 FROM workspaces
574 WHERE ssh_project_id = ?
575 })
576 .and_then(|mut prepared_statement| (prepared_statement)(ssh_project.id.0))
577 .context("No workspaces found")
578 .warn_on_err()
579 .flatten()?;
580
581 Some(SerializedWorkspace {
582 id: workspace_id,
583 location: SerializedWorkspaceLocation::Ssh(ssh_project.clone()),
584 center_group: self
585 .get_center_pane_group(workspace_id)
586 .context("Getting center group")
587 .log_err()?,
588 window_bounds,
589 centered_layout: centered_layout.unwrap_or(false),
590 display,
591 docks,
592 session_id: None,
593 window_id,
594 })
595 }
596
597 /// Saves a workspace using the worktree roots. Will garbage collect any workspaces
598 /// that used this workspace previously
599 pub(crate) async fn save_workspace(&self, workspace: SerializedWorkspace) {
600 self.write(move |conn| {
601 conn.with_savepoint("update_worktrees", || {
602 // Clear out panes and pane_groups
603 conn.exec_bound(sql!(
604 DELETE FROM pane_groups WHERE workspace_id = ?1;
605 DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)
606 .context("Clearing old panes")?;
607
608 match workspace.location {
609 SerializedWorkspaceLocation::Local(local_paths, local_paths_order) => {
610 conn.exec_bound(sql!(
611 DELETE FROM workspaces WHERE local_paths = ? AND workspace_id != ?
612 ))?((&local_paths, workspace.id))
613 .context("clearing out old locations")?;
614
615 // Upsert
616 let query = sql!(
617 INSERT INTO workspaces(
618 workspace_id,
619 local_paths,
620 local_paths_order,
621 left_dock_visible,
622 left_dock_active_panel,
623 left_dock_zoom,
624 right_dock_visible,
625 right_dock_active_panel,
626 right_dock_zoom,
627 bottom_dock_visible,
628 bottom_dock_active_panel,
629 bottom_dock_zoom,
630 session_id,
631 window_id,
632 timestamp
633 )
634 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, CURRENT_TIMESTAMP)
635 ON CONFLICT DO
636 UPDATE SET
637 local_paths = ?2,
638 local_paths_order = ?3,
639 left_dock_visible = ?4,
640 left_dock_active_panel = ?5,
641 left_dock_zoom = ?6,
642 right_dock_visible = ?7,
643 right_dock_active_panel = ?8,
644 right_dock_zoom = ?9,
645 bottom_dock_visible = ?10,
646 bottom_dock_active_panel = ?11,
647 bottom_dock_zoom = ?12,
648 session_id = ?13,
649 window_id = ?14,
650 timestamp = CURRENT_TIMESTAMP
651 );
652 let mut prepared_query = conn.exec_bound(query)?;
653 let args = (workspace.id, &local_paths, &local_paths_order, workspace.docks, workspace.session_id, workspace.window_id);
654
655 prepared_query(args).context("Updating workspace")?;
656 }
657 SerializedWorkspaceLocation::DevServer(dev_server_project) => {
658 conn.exec_bound(sql!(
659 DELETE FROM workspaces WHERE dev_server_project_id = ? AND workspace_id != ?
660 ))?((dev_server_project.id.0, workspace.id))
661 .context("clearing out old locations")?;
662
663 conn.exec_bound(sql!(
664 INSERT INTO dev_server_projects(
665 id,
666 path,
667 dev_server_name
668 ) VALUES (?1, ?2, ?3)
669 ON CONFLICT DO
670 UPDATE SET
671 path = ?2,
672 dev_server_name = ?3
673 ))?(&dev_server_project)?;
674
675 // Upsert
676 conn.exec_bound(sql!(
677 INSERT INTO workspaces(
678 workspace_id,
679 dev_server_project_id,
680 left_dock_visible,
681 left_dock_active_panel,
682 left_dock_zoom,
683 right_dock_visible,
684 right_dock_active_panel,
685 right_dock_zoom,
686 bottom_dock_visible,
687 bottom_dock_active_panel,
688 bottom_dock_zoom,
689 timestamp
690 )
691 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, CURRENT_TIMESTAMP)
692 ON CONFLICT DO
693 UPDATE SET
694 dev_server_project_id = ?2,
695 left_dock_visible = ?3,
696 left_dock_active_panel = ?4,
697 left_dock_zoom = ?5,
698 right_dock_visible = ?6,
699 right_dock_active_panel = ?7,
700 right_dock_zoom = ?8,
701 bottom_dock_visible = ?9,
702 bottom_dock_active_panel = ?10,
703 bottom_dock_zoom = ?11,
704 timestamp = CURRENT_TIMESTAMP
705 ))?((
706 workspace.id,
707 dev_server_project.id.0,
708 workspace.docks,
709 ))
710 .context("Updating workspace")?;
711 },
712 SerializedWorkspaceLocation::Ssh(ssh_project) => {
713 conn.exec_bound(sql!(
714 DELETE FROM workspaces WHERE ssh_project_id = ? AND workspace_id != ?
715 ))?((ssh_project.id.0, workspace.id))
716 .context("clearing out old locations")?;
717
718 // Upsert
719 conn.exec_bound(sql!(
720 INSERT INTO workspaces(
721 workspace_id,
722 ssh_project_id,
723 left_dock_visible,
724 left_dock_active_panel,
725 left_dock_zoom,
726 right_dock_visible,
727 right_dock_active_panel,
728 right_dock_zoom,
729 bottom_dock_visible,
730 bottom_dock_active_panel,
731 bottom_dock_zoom,
732 timestamp
733 )
734 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, CURRENT_TIMESTAMP)
735 ON CONFLICT DO
736 UPDATE SET
737 ssh_project_id = ?2,
738 left_dock_visible = ?3,
739 left_dock_active_panel = ?4,
740 left_dock_zoom = ?5,
741 right_dock_visible = ?6,
742 right_dock_active_panel = ?7,
743 right_dock_zoom = ?8,
744 bottom_dock_visible = ?9,
745 bottom_dock_active_panel = ?10,
746 bottom_dock_zoom = ?11,
747 timestamp = CURRENT_TIMESTAMP
748 ))?((
749 workspace.id,
750 ssh_project.id.0,
751 workspace.docks,
752 ))
753 .context("Updating workspace")?;
754 }
755 }
756
757 // Save center pane group
758 Self::save_pane_group(conn, workspace.id, &workspace.center_group, None)
759 .context("save pane group in save workspace")?;
760
761 Ok(())
762 })
763 .log_err();
764 })
765 .await;
766 }
767
768 pub(crate) async fn get_or_create_ssh_project(
769 &self,
770 host: String,
771 port: Option<u16>,
772 path: String,
773 user: Option<String>,
774 ) -> Result<SerializedSshProject> {
775 if let Some(project) = self
776 .get_ssh_project(host.clone(), port, path.clone(), user.clone())
777 .await?
778 {
779 Ok(project)
780 } else {
781 self.insert_ssh_project(host, port, path, user)
782 .await?
783 .ok_or_else(|| anyhow!("failed to insert ssh project"))
784 }
785 }
786
787 query! {
788 async fn get_ssh_project(host: String, port: Option<u16>, path: String, user: Option<String>) -> Result<Option<SerializedSshProject>> {
789 SELECT id, host, port, path, user
790 FROM ssh_projects
791 WHERE host IS ? AND port IS ? AND path IS ? AND user IS ?
792 LIMIT 1
793 }
794 }
795
796 query! {
797 async fn insert_ssh_project(host: String, port: Option<u16>, path: String, user: Option<String>) -> Result<Option<SerializedSshProject>> {
798 INSERT INTO ssh_projects(
799 host,
800 port,
801 path,
802 user
803 ) VALUES (?1, ?2, ?3, ?4)
804 RETURNING id, host, port, path, user
805 }
806 }
807
808 query! {
809 pub async fn next_id() -> Result<WorkspaceId> {
810 INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id
811 }
812 }
813
814 query! {
815 fn recent_workspaces() -> Result<Vec<(WorkspaceId, LocalPaths, LocalPathsOrder, Option<u64>, Option<u64>)>> {
816 SELECT workspace_id, local_paths, local_paths_order, dev_server_project_id, ssh_project_id
817 FROM workspaces
818 WHERE local_paths IS NOT NULL
819 OR dev_server_project_id IS NOT NULL
820 OR ssh_project_id IS NOT NULL
821 ORDER BY timestamp DESC
822 }
823 }
824
825 query! {
826 fn session_workspaces(session_id: String) -> Result<Vec<(LocalPaths, Option<u64>)>> {
827 SELECT local_paths, window_id
828 FROM workspaces
829 WHERE session_id = ?1 AND dev_server_project_id IS NULL
830 ORDER BY timestamp DESC
831 }
832 }
833
834 query! {
835 fn dev_server_projects() -> Result<Vec<SerializedDevServerProject>> {
836 SELECT id, path, dev_server_name
837 FROM dev_server_projects
838 }
839 }
840
841 query! {
842 fn ssh_projects() -> Result<Vec<SerializedSshProject>> {
843 SELECT id, host, port, path, user
844 FROM ssh_projects
845 }
846 }
847
848 pub(crate) fn last_window(
849 &self,
850 ) -> anyhow::Result<(Option<Uuid>, Option<SerializedWindowBounds>)> {
851 let mut prepared_query =
852 self.select::<(Option<Uuid>, Option<SerializedWindowBounds>)>(sql!(
853 SELECT
854 display,
855 window_state, window_x, window_y, window_width, window_height
856 FROM workspaces
857 WHERE local_paths
858 IS NOT NULL
859 ORDER BY timestamp DESC
860 LIMIT 1
861 ))?;
862 let result = prepared_query()?;
863 Ok(result.into_iter().next().unwrap_or((None, None)))
864 }
865
866 query! {
867 pub async fn delete_workspace_by_id(id: WorkspaceId) -> Result<()> {
868 DELETE FROM workspaces
869 WHERE workspace_id IS ?
870 }
871 }
872
873 pub async fn delete_workspace_by_dev_server_project_id(
874 &self,
875 id: DevServerProjectId,
876 ) -> Result<()> {
877 self.write(move |conn| {
878 conn.exec_bound(sql!(
879 DELETE FROM dev_server_projects WHERE id = ?
880 ))?(id.0)?;
881 conn.exec_bound(sql!(
882 DELETE FROM workspaces
883 WHERE dev_server_project_id IS ?
884 ))?(id.0)
885 })
886 .await
887 }
888
889 // Returns the recent locations which are still valid on disk and deletes ones which no longer
890 // exist.
891 pub async fn recent_workspaces_on_disk(
892 &self,
893 ) -> Result<Vec<(WorkspaceId, SerializedWorkspaceLocation)>> {
894 let mut result = Vec::new();
895 let mut delete_tasks = Vec::new();
896 let dev_server_projects = self.dev_server_projects()?;
897 let ssh_projects = self.ssh_projects()?;
898
899 for (id, location, order, dev_server_project_id, ssh_project_id) in
900 self.recent_workspaces()?
901 {
902 if let Some(dev_server_project_id) = dev_server_project_id.map(DevServerProjectId) {
903 if let Some(dev_server_project) = dev_server_projects
904 .iter()
905 .find(|rp| rp.id == dev_server_project_id)
906 {
907 result.push((id, dev_server_project.clone().into()));
908 } else {
909 delete_tasks.push(self.delete_workspace_by_id(id));
910 }
911 continue;
912 }
913
914 if let Some(ssh_project_id) = ssh_project_id.map(SshProjectId) {
915 if let Some(ssh_project) = ssh_projects.iter().find(|rp| rp.id == ssh_project_id) {
916 result.push((id, SerializedWorkspaceLocation::Ssh(ssh_project.clone())));
917 } else {
918 delete_tasks.push(self.delete_workspace_by_id(id));
919 }
920 continue;
921 }
922
923 if location.paths().iter().all(|path| path.exists())
924 && location.paths().iter().any(|path| path.is_dir())
925 {
926 result.push((id, SerializedWorkspaceLocation::Local(location, order)));
927 } else {
928 delete_tasks.push(self.delete_workspace_by_id(id));
929 }
930 }
931
932 futures::future::join_all(delete_tasks).await;
933 Ok(result)
934 }
935
936 pub async fn last_workspace(&self) -> Result<Option<LocalPaths>> {
937 Ok(self
938 .recent_workspaces_on_disk()
939 .await?
940 .into_iter()
941 .filter_map(|(_, location)| match location {
942 SerializedWorkspaceLocation::Local(local_paths, _) => Some(local_paths),
943 // Do not automatically reopen Dev Server and SSH workspaces
944 SerializedWorkspaceLocation::DevServer(_) => None,
945 SerializedWorkspaceLocation::Ssh(_) => None,
946 })
947 .next())
948 }
949
950 // Returns the locations of the workspaces that were still opened when the last
951 // session was closed (i.e. when Zed was quit).
952 // If `last_session_window_order` is provided, the returned locations are ordered
953 // according to that.
954 pub fn last_session_workspace_locations(
955 &self,
956 last_session_id: &str,
957 last_session_window_stack: Option<Vec<WindowId>>,
958 ) -> Result<Vec<LocalPaths>> {
959 let mut workspaces = Vec::new();
960
961 for (location, window_id) in self.session_workspaces(last_session_id.to_owned())? {
962 if location.paths().iter().all(|path| path.exists())
963 && location.paths().iter().any(|path| path.is_dir())
964 {
965 workspaces.push((location, window_id.map(WindowId::from)));
966 }
967 }
968
969 if let Some(stack) = last_session_window_stack {
970 workspaces.sort_by_key(|(_, window_id)| {
971 window_id
972 .and_then(|id| stack.iter().position(|&order_id| order_id == id))
973 .unwrap_or(usize::MAX)
974 });
975 }
976
977 Ok(workspaces
978 .into_iter()
979 .map(|(paths, _)| paths)
980 .collect::<Vec<_>>())
981 }
982
983 fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
984 Ok(self
985 .get_pane_group(workspace_id, None)?
986 .into_iter()
987 .next()
988 .unwrap_or_else(|| {
989 SerializedPaneGroup::Pane(SerializedPane {
990 active: true,
991 children: vec![],
992 pinned_count: 0,
993 })
994 }))
995 }
996
997 fn get_pane_group(
998 &self,
999 workspace_id: WorkspaceId,
1000 group_id: Option<GroupId>,
1001 ) -> Result<Vec<SerializedPaneGroup>> {
1002 type GroupKey = (Option<GroupId>, WorkspaceId);
1003 type GroupOrPane = (
1004 Option<GroupId>,
1005 Option<SerializedAxis>,
1006 Option<PaneId>,
1007 Option<bool>,
1008 Option<usize>,
1009 Option<String>,
1010 );
1011 self.select_bound::<GroupKey, GroupOrPane>(sql!(
1012 SELECT group_id, axis, pane_id, active, pinned_count, flexes
1013 FROM (SELECT
1014 group_id,
1015 axis,
1016 NULL as pane_id,
1017 NULL as active,
1018 NULL as pinned_count,
1019 position,
1020 parent_group_id,
1021 workspace_id,
1022 flexes
1023 FROM pane_groups
1024 UNION
1025 SELECT
1026 NULL,
1027 NULL,
1028 center_panes.pane_id,
1029 panes.active as active,
1030 pinned_count,
1031 position,
1032 parent_group_id,
1033 panes.workspace_id as workspace_id,
1034 NULL
1035 FROM center_panes
1036 JOIN panes ON center_panes.pane_id = panes.pane_id)
1037 WHERE parent_group_id IS ? AND workspace_id = ?
1038 ORDER BY position
1039 ))?((group_id, workspace_id))?
1040 .into_iter()
1041 .map(|(group_id, axis, pane_id, active, pinned_count, flexes)| {
1042 let maybe_pane = maybe!({ Some((pane_id?, active?, pinned_count?)) });
1043 if let Some((group_id, axis)) = group_id.zip(axis) {
1044 let flexes = flexes
1045 .map(|flexes: String| serde_json::from_str::<Vec<f32>>(&flexes))
1046 .transpose()?;
1047
1048 Ok(SerializedPaneGroup::Group {
1049 axis,
1050 children: self.get_pane_group(workspace_id, Some(group_id))?,
1051 flexes,
1052 })
1053 } else if let Some((pane_id, active, pinned_count)) = maybe_pane {
1054 Ok(SerializedPaneGroup::Pane(SerializedPane::new(
1055 self.get_items(pane_id)?,
1056 active,
1057 pinned_count,
1058 )))
1059 } else {
1060 bail!("Pane Group Child was neither a pane group or a pane");
1061 }
1062 })
1063 // Filter out panes and pane groups which don't have any children or items
1064 .filter(|pane_group| match pane_group {
1065 Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
1066 Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
1067 _ => true,
1068 })
1069 .collect::<Result<_>>()
1070 }
1071
1072 fn save_pane_group(
1073 conn: &Connection,
1074 workspace_id: WorkspaceId,
1075 pane_group: &SerializedPaneGroup,
1076 parent: Option<(GroupId, usize)>,
1077 ) -> Result<()> {
1078 match pane_group {
1079 SerializedPaneGroup::Group {
1080 axis,
1081 children,
1082 flexes,
1083 } => {
1084 let (parent_id, position) = parent.unzip();
1085
1086 let flex_string = flexes
1087 .as_ref()
1088 .map(|flexes| serde_json::json!(flexes).to_string());
1089
1090 let group_id = conn.select_row_bound::<_, i64>(sql!(
1091 INSERT INTO pane_groups(
1092 workspace_id,
1093 parent_group_id,
1094 position,
1095 axis,
1096 flexes
1097 )
1098 VALUES (?, ?, ?, ?, ?)
1099 RETURNING group_id
1100 ))?((
1101 workspace_id,
1102 parent_id,
1103 position,
1104 *axis,
1105 flex_string,
1106 ))?
1107 .ok_or_else(|| anyhow!("Couldn't retrieve group_id from inserted pane_group"))?;
1108
1109 for (position, group) in children.iter().enumerate() {
1110 Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))?
1111 }
1112
1113 Ok(())
1114 }
1115 SerializedPaneGroup::Pane(pane) => {
1116 Self::save_pane(conn, workspace_id, pane, parent)?;
1117 Ok(())
1118 }
1119 }
1120 }
1121
1122 fn save_pane(
1123 conn: &Connection,
1124 workspace_id: WorkspaceId,
1125 pane: &SerializedPane,
1126 parent: Option<(GroupId, usize)>,
1127 ) -> Result<PaneId> {
1128 let pane_id = conn.select_row_bound::<_, i64>(sql!(
1129 INSERT INTO panes(workspace_id, active, pinned_count)
1130 VALUES (?, ?, ?)
1131 RETURNING pane_id
1132 ))?((workspace_id, pane.active, pane.pinned_count))?
1133 .ok_or_else(|| anyhow!("Could not retrieve inserted pane_id"))?;
1134
1135 let (parent_id, order) = parent.unzip();
1136 conn.exec_bound(sql!(
1137 INSERT INTO center_panes(pane_id, parent_group_id, position)
1138 VALUES (?, ?, ?)
1139 ))?((pane_id, parent_id, order))?;
1140
1141 Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
1142
1143 Ok(pane_id)
1144 }
1145
1146 fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
1147 self.select_bound(sql!(
1148 SELECT kind, item_id, active, preview FROM items
1149 WHERE pane_id = ?
1150 ORDER BY position
1151 ))?(pane_id)
1152 }
1153
1154 fn save_items(
1155 conn: &Connection,
1156 workspace_id: WorkspaceId,
1157 pane_id: PaneId,
1158 items: &[SerializedItem],
1159 ) -> Result<()> {
1160 let mut insert = conn.exec_bound(sql!(
1161 INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active, preview) VALUES (?, ?, ?, ?, ?, ?, ?)
1162 )).context("Preparing insertion")?;
1163 for (position, item) in items.iter().enumerate() {
1164 insert((workspace_id, pane_id, position, item))?;
1165 }
1166
1167 Ok(())
1168 }
1169
1170 query! {
1171 pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> {
1172 UPDATE workspaces
1173 SET timestamp = CURRENT_TIMESTAMP
1174 WHERE workspace_id = ?
1175 }
1176 }
1177
1178 query! {
1179 pub(crate) async fn set_window_open_status(workspace_id: WorkspaceId, bounds: SerializedWindowBounds, display: Uuid) -> Result<()> {
1180 UPDATE workspaces
1181 SET window_state = ?2,
1182 window_x = ?3,
1183 window_y = ?4,
1184 window_width = ?5,
1185 window_height = ?6,
1186 display = ?7
1187 WHERE workspace_id = ?1
1188 }
1189 }
1190
1191 query! {
1192 pub(crate) async fn set_centered_layout(workspace_id: WorkspaceId, centered_layout: bool) -> Result<()> {
1193 UPDATE workspaces
1194 SET centered_layout = ?2
1195 WHERE workspace_id = ?1
1196 }
1197 }
1198}
1199
1200#[cfg(test)]
1201mod tests {
1202 use super::*;
1203 use crate::persistence::model::SerializedWorkspace;
1204 use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
1205 use db::open_test_db;
1206 use gpui::{self};
1207
1208 #[gpui::test]
1209 async fn test_next_id_stability() {
1210 env_logger::try_init().ok();
1211
1212 let db = WorkspaceDb(open_test_db("test_next_id_stability").await);
1213
1214 db.write(|conn| {
1215 conn.migrate(
1216 "test_table",
1217 &[sql!(
1218 CREATE TABLE test_table(
1219 text TEXT,
1220 workspace_id INTEGER,
1221 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
1222 ON DELETE CASCADE
1223 ) STRICT;
1224 )],
1225 )
1226 .unwrap();
1227 })
1228 .await;
1229
1230 let id = db.next_id().await.unwrap();
1231 // Assert the empty row got inserted
1232 assert_eq!(
1233 Some(id),
1234 db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
1235 SELECT workspace_id FROM workspaces WHERE workspace_id = ?
1236 ))
1237 .unwrap()(id)
1238 .unwrap()
1239 );
1240
1241 db.write(move |conn| {
1242 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
1243 .unwrap()(("test-text-1", id))
1244 .unwrap()
1245 })
1246 .await;
1247
1248 let test_text_1 = db
1249 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
1250 .unwrap()(1)
1251 .unwrap()
1252 .unwrap();
1253 assert_eq!(test_text_1, "test-text-1");
1254 }
1255
1256 #[gpui::test]
1257 async fn test_workspace_id_stability() {
1258 env_logger::try_init().ok();
1259
1260 let db = WorkspaceDb(open_test_db("test_workspace_id_stability").await);
1261
1262 db.write(|conn| {
1263 conn.migrate(
1264 "test_table",
1265 &[sql!(
1266 CREATE TABLE test_table(
1267 text TEXT,
1268 workspace_id INTEGER,
1269 FOREIGN KEY(workspace_id)
1270 REFERENCES workspaces(workspace_id)
1271 ON DELETE CASCADE
1272 ) STRICT;)],
1273 )
1274 })
1275 .await
1276 .unwrap();
1277
1278 let mut workspace_1 = SerializedWorkspace {
1279 id: WorkspaceId(1),
1280 location: SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp2"]),
1281 center_group: Default::default(),
1282 window_bounds: Default::default(),
1283 display: Default::default(),
1284 docks: Default::default(),
1285 centered_layout: false,
1286 session_id: None,
1287 window_id: None,
1288 };
1289
1290 let workspace_2 = SerializedWorkspace {
1291 id: WorkspaceId(2),
1292 location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]),
1293 center_group: Default::default(),
1294 window_bounds: Default::default(),
1295 display: Default::default(),
1296 docks: Default::default(),
1297 centered_layout: false,
1298 session_id: None,
1299 window_id: None,
1300 };
1301
1302 db.save_workspace(workspace_1.clone()).await;
1303
1304 db.write(|conn| {
1305 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
1306 .unwrap()(("test-text-1", 1))
1307 .unwrap();
1308 })
1309 .await;
1310
1311 db.save_workspace(workspace_2.clone()).await;
1312
1313 db.write(|conn| {
1314 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
1315 .unwrap()(("test-text-2", 2))
1316 .unwrap();
1317 })
1318 .await;
1319
1320 workspace_1.location = SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp3"]);
1321 db.save_workspace(workspace_1.clone()).await;
1322 db.save_workspace(workspace_1).await;
1323 db.save_workspace(workspace_2).await;
1324
1325 let test_text_2 = db
1326 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
1327 .unwrap()(2)
1328 .unwrap()
1329 .unwrap();
1330 assert_eq!(test_text_2, "test-text-2");
1331
1332 let test_text_1 = db
1333 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
1334 .unwrap()(1)
1335 .unwrap()
1336 .unwrap();
1337 assert_eq!(test_text_1, "test-text-1");
1338 }
1339
1340 fn group(axis: Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
1341 SerializedPaneGroup::Group {
1342 axis: SerializedAxis(axis),
1343 flexes: None,
1344 children,
1345 }
1346 }
1347
1348 #[gpui::test]
1349 async fn test_full_workspace_serialization() {
1350 env_logger::try_init().ok();
1351
1352 let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await);
1353
1354 // -----------------
1355 // | 1,2 | 5,6 |
1356 // | - - - | |
1357 // | 3,4 | |
1358 // -----------------
1359 let center_group = group(
1360 Axis::Horizontal,
1361 vec![
1362 group(
1363 Axis::Vertical,
1364 vec![
1365 SerializedPaneGroup::Pane(SerializedPane::new(
1366 vec![
1367 SerializedItem::new("Terminal", 5, false, false),
1368 SerializedItem::new("Terminal", 6, true, false),
1369 ],
1370 false,
1371 0,
1372 )),
1373 SerializedPaneGroup::Pane(SerializedPane::new(
1374 vec![
1375 SerializedItem::new("Terminal", 7, true, false),
1376 SerializedItem::new("Terminal", 8, false, false),
1377 ],
1378 false,
1379 0,
1380 )),
1381 ],
1382 ),
1383 SerializedPaneGroup::Pane(SerializedPane::new(
1384 vec![
1385 SerializedItem::new("Terminal", 9, false, false),
1386 SerializedItem::new("Terminal", 10, true, false),
1387 ],
1388 false,
1389 0,
1390 )),
1391 ],
1392 );
1393
1394 let workspace = SerializedWorkspace {
1395 id: WorkspaceId(5),
1396 location: SerializedWorkspaceLocation::Local(
1397 LocalPaths::new(["/tmp", "/tmp2"]),
1398 LocalPathsOrder::new([1, 0]),
1399 ),
1400 center_group,
1401 window_bounds: Default::default(),
1402 display: Default::default(),
1403 docks: Default::default(),
1404 centered_layout: false,
1405 session_id: None,
1406 window_id: Some(999),
1407 };
1408
1409 db.save_workspace(workspace.clone()).await;
1410
1411 let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
1412 assert_eq!(workspace, round_trip_workspace.unwrap());
1413
1414 // Test guaranteed duplicate IDs
1415 db.save_workspace(workspace.clone()).await;
1416 db.save_workspace(workspace.clone()).await;
1417
1418 let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
1419 assert_eq!(workspace, round_trip_workspace.unwrap());
1420 }
1421
1422 #[gpui::test]
1423 async fn test_workspace_assignment() {
1424 env_logger::try_init().ok();
1425
1426 let db = WorkspaceDb(open_test_db("test_basic_functionality").await);
1427
1428 let workspace_1 = SerializedWorkspace {
1429 id: WorkspaceId(1),
1430 location: SerializedWorkspaceLocation::Local(
1431 LocalPaths::new(["/tmp", "/tmp2"]),
1432 LocalPathsOrder::new([0, 1]),
1433 ),
1434 center_group: Default::default(),
1435 window_bounds: Default::default(),
1436 display: Default::default(),
1437 docks: Default::default(),
1438 centered_layout: false,
1439 session_id: None,
1440 window_id: Some(1),
1441 };
1442
1443 let mut workspace_2 = SerializedWorkspace {
1444 id: WorkspaceId(2),
1445 location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]),
1446 center_group: Default::default(),
1447 window_bounds: Default::default(),
1448 display: Default::default(),
1449 docks: Default::default(),
1450 centered_layout: false,
1451 session_id: None,
1452 window_id: Some(2),
1453 };
1454
1455 db.save_workspace(workspace_1.clone()).await;
1456 db.save_workspace(workspace_2.clone()).await;
1457
1458 // Test that paths are treated as a set
1459 assert_eq!(
1460 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
1461 workspace_1
1462 );
1463 assert_eq!(
1464 db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
1465 workspace_1
1466 );
1467
1468 // Make sure that other keys work
1469 assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
1470 assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
1471
1472 // Test 'mutate' case of updating a pre-existing id
1473 workspace_2.location = SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp2"]);
1474
1475 db.save_workspace(workspace_2.clone()).await;
1476 assert_eq!(
1477 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
1478 workspace_2
1479 );
1480
1481 // Test other mechanism for mutating
1482 let mut workspace_3 = SerializedWorkspace {
1483 id: WorkspaceId(3),
1484 location: SerializedWorkspaceLocation::Local(
1485 LocalPaths::new(["/tmp", "/tmp2"]),
1486 LocalPathsOrder::new([1, 0]),
1487 ),
1488 center_group: Default::default(),
1489 window_bounds: Default::default(),
1490 display: Default::default(),
1491 docks: Default::default(),
1492 centered_layout: false,
1493 session_id: None,
1494 window_id: Some(3),
1495 };
1496
1497 db.save_workspace(workspace_3.clone()).await;
1498 assert_eq!(
1499 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
1500 workspace_3
1501 );
1502
1503 // Make sure that updating paths differently also works
1504 workspace_3.location =
1505 SerializedWorkspaceLocation::from_local_paths(["/tmp3", "/tmp4", "/tmp2"]);
1506 db.save_workspace(workspace_3.clone()).await;
1507 assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
1508 assert_eq!(
1509 db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
1510 .unwrap(),
1511 workspace_3
1512 );
1513 }
1514
1515 #[gpui::test]
1516 async fn test_session_workspaces() {
1517 env_logger::try_init().ok();
1518
1519 let db = WorkspaceDb(open_test_db("test_serializing_workspaces_session_id").await);
1520
1521 let workspace_1 = SerializedWorkspace {
1522 id: WorkspaceId(1),
1523 location: SerializedWorkspaceLocation::from_local_paths(["/tmp1"]),
1524 center_group: Default::default(),
1525 window_bounds: Default::default(),
1526 display: Default::default(),
1527 docks: Default::default(),
1528 centered_layout: false,
1529 session_id: Some("session-id-1".to_owned()),
1530 window_id: Some(10),
1531 };
1532
1533 let workspace_2 = SerializedWorkspace {
1534 id: WorkspaceId(2),
1535 location: SerializedWorkspaceLocation::from_local_paths(["/tmp2"]),
1536 center_group: Default::default(),
1537 window_bounds: Default::default(),
1538 display: Default::default(),
1539 docks: Default::default(),
1540 centered_layout: false,
1541 session_id: Some("session-id-1".to_owned()),
1542 window_id: Some(20),
1543 };
1544
1545 let workspace_3 = SerializedWorkspace {
1546 id: WorkspaceId(3),
1547 location: SerializedWorkspaceLocation::from_local_paths(["/tmp3"]),
1548 center_group: Default::default(),
1549 window_bounds: Default::default(),
1550 display: Default::default(),
1551 docks: Default::default(),
1552 centered_layout: false,
1553 session_id: Some("session-id-2".to_owned()),
1554 window_id: Some(30),
1555 };
1556
1557 let workspace_4 = SerializedWorkspace {
1558 id: WorkspaceId(4),
1559 location: SerializedWorkspaceLocation::from_local_paths(["/tmp4"]),
1560 center_group: Default::default(),
1561 window_bounds: Default::default(),
1562 display: Default::default(),
1563 docks: Default::default(),
1564 centered_layout: false,
1565 session_id: None,
1566 window_id: None,
1567 };
1568
1569 db.save_workspace(workspace_1.clone()).await;
1570 db.save_workspace(workspace_2.clone()).await;
1571 db.save_workspace(workspace_3.clone()).await;
1572 db.save_workspace(workspace_4.clone()).await;
1573
1574 let locations = db.session_workspaces("session-id-1".to_owned()).unwrap();
1575 assert_eq!(locations.len(), 2);
1576 assert_eq!(locations[0].0, LocalPaths::new(["/tmp1"]));
1577 assert_eq!(locations[0].1, Some(10));
1578 assert_eq!(locations[1].0, LocalPaths::new(["/tmp2"]));
1579 assert_eq!(locations[1].1, Some(20));
1580
1581 let locations = db.session_workspaces("session-id-2".to_owned()).unwrap();
1582 assert_eq!(locations.len(), 1);
1583 assert_eq!(locations[0].0, LocalPaths::new(["/tmp3"]));
1584 assert_eq!(locations[0].1, Some(30));
1585 }
1586
1587 fn default_workspace<P: AsRef<Path>>(
1588 workspace_id: &[P],
1589 center_group: &SerializedPaneGroup,
1590 ) -> SerializedWorkspace {
1591 SerializedWorkspace {
1592 id: WorkspaceId(4),
1593 location: SerializedWorkspaceLocation::from_local_paths(workspace_id),
1594 center_group: center_group.clone(),
1595 window_bounds: Default::default(),
1596 display: Default::default(),
1597 docks: Default::default(),
1598 centered_layout: false,
1599 session_id: None,
1600 window_id: None,
1601 }
1602 }
1603
1604 #[gpui::test]
1605 async fn test_last_session_workspace_locations() {
1606 let dir1 = tempfile::TempDir::with_prefix("dir1").unwrap();
1607 let dir2 = tempfile::TempDir::with_prefix("dir2").unwrap();
1608 let dir3 = tempfile::TempDir::with_prefix("dir3").unwrap();
1609 let dir4 = tempfile::TempDir::with_prefix("dir4").unwrap();
1610
1611 let db =
1612 WorkspaceDb(open_test_db("test_serializing_workspaces_last_session_workspaces").await);
1613
1614 let workspaces = [
1615 (1, dir1.path().to_str().unwrap(), 9),
1616 (2, dir2.path().to_str().unwrap(), 5),
1617 (3, dir3.path().to_str().unwrap(), 8),
1618 (4, dir4.path().to_str().unwrap(), 2),
1619 ]
1620 .into_iter()
1621 .map(|(id, location, window_id)| SerializedWorkspace {
1622 id: WorkspaceId(id),
1623 location: SerializedWorkspaceLocation::from_local_paths([location]),
1624 center_group: Default::default(),
1625 window_bounds: Default::default(),
1626 display: Default::default(),
1627 docks: Default::default(),
1628 centered_layout: false,
1629 session_id: Some("one-session".to_owned()),
1630 window_id: Some(window_id),
1631 })
1632 .collect::<Vec<_>>();
1633
1634 for workspace in workspaces.iter() {
1635 db.save_workspace(workspace.clone()).await;
1636 }
1637
1638 let stack = Some(Vec::from([
1639 WindowId::from(2), // Top
1640 WindowId::from(8),
1641 WindowId::from(5),
1642 WindowId::from(9), // Bottom
1643 ]));
1644
1645 let have = db
1646 .last_session_workspace_locations("one-session", stack)
1647 .unwrap();
1648 assert_eq!(have.len(), 4);
1649 assert_eq!(have[0], LocalPaths::new([dir4.path().to_str().unwrap()]));
1650 assert_eq!(have[1], LocalPaths::new([dir3.path().to_str().unwrap()]));
1651 assert_eq!(have[2], LocalPaths::new([dir2.path().to_str().unwrap()]));
1652 assert_eq!(have[3], LocalPaths::new([dir1.path().to_str().unwrap()]));
1653 }
1654
1655 #[gpui::test]
1656 async fn test_get_or_create_ssh_project() {
1657 let db = WorkspaceDb(open_test_db("test_get_or_create_ssh_project").await);
1658
1659 let (host, port, path, user) = (
1660 "example.com".to_string(),
1661 Some(22_u16),
1662 "/home/user".to_string(),
1663 Some("user".to_string()),
1664 );
1665
1666 let project = db
1667 .get_or_create_ssh_project(host.clone(), port, path.clone(), user.clone())
1668 .await
1669 .unwrap();
1670
1671 assert_eq!(project.host, host);
1672 assert_eq!(project.path, path);
1673 assert_eq!(project.user, user);
1674
1675 // Test that calling the function again with the same parameters returns the same project
1676 let same_project = db
1677 .get_or_create_ssh_project(host.clone(), port, path.clone(), user.clone())
1678 .await
1679 .unwrap();
1680
1681 assert_eq!(project.id, same_project.id);
1682
1683 // Test with different parameters
1684 let (host2, path2, user2) = (
1685 "otherexample.com".to_string(),
1686 "/home/otheruser".to_string(),
1687 Some("otheruser".to_string()),
1688 );
1689
1690 let different_project = db
1691 .get_or_create_ssh_project(host2.clone(), None, path2.clone(), user2.clone())
1692 .await
1693 .unwrap();
1694
1695 assert_ne!(project.id, different_project.id);
1696 assert_eq!(different_project.host, host2);
1697 assert_eq!(different_project.path, path2);
1698 assert_eq!(different_project.user, user2);
1699 }
1700
1701 #[gpui::test]
1702 async fn test_get_or_create_ssh_project_with_null_user() {
1703 let db = WorkspaceDb(open_test_db("test_get_or_create_ssh_project_with_null_user").await);
1704
1705 let (host, port, path, user) = (
1706 "example.com".to_string(),
1707 None,
1708 "/home/user".to_string(),
1709 None,
1710 );
1711
1712 let project = db
1713 .get_or_create_ssh_project(host.clone(), port, path.clone(), None)
1714 .await
1715 .unwrap();
1716
1717 assert_eq!(project.host, host);
1718 assert_eq!(project.path, path);
1719 assert_eq!(project.user, None);
1720
1721 // Test that calling the function again with the same parameters returns the same project
1722 let same_project = db
1723 .get_or_create_ssh_project(host.clone(), port, path.clone(), user.clone())
1724 .await
1725 .unwrap();
1726
1727 assert_eq!(project.id, same_project.id);
1728 }
1729
1730 #[gpui::test]
1731 async fn test_get_ssh_projects() {
1732 let db = WorkspaceDb(open_test_db("test_get_ssh_projects").await);
1733
1734 let projects = vec![
1735 (
1736 "example.com".to_string(),
1737 None,
1738 "/home/user".to_string(),
1739 None,
1740 ),
1741 (
1742 "anotherexample.com".to_string(),
1743 Some(123_u16),
1744 "/home/user2".to_string(),
1745 Some("user2".to_string()),
1746 ),
1747 (
1748 "yetanother.com".to_string(),
1749 Some(345_u16),
1750 "/home/user3".to_string(),
1751 None,
1752 ),
1753 ];
1754
1755 for (host, port, path, user) in projects.iter() {
1756 let project = db
1757 .get_or_create_ssh_project(host.clone(), *port, path.clone(), user.clone())
1758 .await
1759 .unwrap();
1760
1761 assert_eq!(&project.host, host);
1762 assert_eq!(&project.port, port);
1763 assert_eq!(&project.path, path);
1764 assert_eq!(&project.user, user);
1765 }
1766
1767 let stored_projects = db.ssh_projects().unwrap();
1768 assert_eq!(stored_projects.len(), projects.len());
1769 }
1770
1771 #[gpui::test]
1772 async fn test_simple_split() {
1773 env_logger::try_init().ok();
1774
1775 let db = WorkspaceDb(open_test_db("simple_split").await);
1776
1777 // -----------------
1778 // | 1,2 | 5,6 |
1779 // | - - - | |
1780 // | 3,4 | |
1781 // -----------------
1782 let center_pane = group(
1783 Axis::Horizontal,
1784 vec![
1785 group(
1786 Axis::Vertical,
1787 vec![
1788 SerializedPaneGroup::Pane(SerializedPane::new(
1789 vec![
1790 SerializedItem::new("Terminal", 1, false, false),
1791 SerializedItem::new("Terminal", 2, true, false),
1792 ],
1793 false,
1794 0,
1795 )),
1796 SerializedPaneGroup::Pane(SerializedPane::new(
1797 vec![
1798 SerializedItem::new("Terminal", 4, false, false),
1799 SerializedItem::new("Terminal", 3, true, false),
1800 ],
1801 true,
1802 0,
1803 )),
1804 ],
1805 ),
1806 SerializedPaneGroup::Pane(SerializedPane::new(
1807 vec![
1808 SerializedItem::new("Terminal", 5, true, false),
1809 SerializedItem::new("Terminal", 6, false, false),
1810 ],
1811 false,
1812 0,
1813 )),
1814 ],
1815 );
1816
1817 let workspace = default_workspace(&["/tmp"], ¢er_pane);
1818
1819 db.save_workspace(workspace.clone()).await;
1820
1821 let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
1822
1823 assert_eq!(workspace.center_group, new_workspace.center_group);
1824 }
1825
1826 #[gpui::test]
1827 async fn test_cleanup_panes() {
1828 env_logger::try_init().ok();
1829
1830 let db = WorkspaceDb(open_test_db("test_cleanup_panes").await);
1831
1832 let center_pane = group(
1833 Axis::Horizontal,
1834 vec![
1835 group(
1836 Axis::Vertical,
1837 vec![
1838 SerializedPaneGroup::Pane(SerializedPane::new(
1839 vec![
1840 SerializedItem::new("Terminal", 1, false, false),
1841 SerializedItem::new("Terminal", 2, true, false),
1842 ],
1843 false,
1844 0,
1845 )),
1846 SerializedPaneGroup::Pane(SerializedPane::new(
1847 vec![
1848 SerializedItem::new("Terminal", 4, false, false),
1849 SerializedItem::new("Terminal", 3, true, false),
1850 ],
1851 true,
1852 0,
1853 )),
1854 ],
1855 ),
1856 SerializedPaneGroup::Pane(SerializedPane::new(
1857 vec![
1858 SerializedItem::new("Terminal", 5, false, false),
1859 SerializedItem::new("Terminal", 6, true, false),
1860 ],
1861 false,
1862 0,
1863 )),
1864 ],
1865 );
1866
1867 let id = &["/tmp"];
1868
1869 let mut workspace = default_workspace(id, ¢er_pane);
1870
1871 db.save_workspace(workspace.clone()).await;
1872
1873 workspace.center_group = group(
1874 Axis::Vertical,
1875 vec![
1876 SerializedPaneGroup::Pane(SerializedPane::new(
1877 vec![
1878 SerializedItem::new("Terminal", 1, false, false),
1879 SerializedItem::new("Terminal", 2, true, false),
1880 ],
1881 false,
1882 0,
1883 )),
1884 SerializedPaneGroup::Pane(SerializedPane::new(
1885 vec![
1886 SerializedItem::new("Terminal", 4, true, false),
1887 SerializedItem::new("Terminal", 3, false, false),
1888 ],
1889 true,
1890 0,
1891 )),
1892 ],
1893 );
1894
1895 db.save_workspace(workspace.clone()).await;
1896
1897 let new_workspace = db.workspace_for_roots(id).unwrap();
1898
1899 assert_eq!(workspace.center_group, new_workspace.center_group);
1900 }
1901}