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