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 session_id,
736 window_id,
737 timestamp
738 )
739 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, CURRENT_TIMESTAMP)
740 ON CONFLICT DO
741 UPDATE SET
742 ssh_project_id = ?2,
743 left_dock_visible = ?3,
744 left_dock_active_panel = ?4,
745 left_dock_zoom = ?5,
746 right_dock_visible = ?6,
747 right_dock_active_panel = ?7,
748 right_dock_zoom = ?8,
749 bottom_dock_visible = ?9,
750 bottom_dock_active_panel = ?10,
751 bottom_dock_zoom = ?11,
752 session_id = ?12,
753 window_id = ?13,
754 timestamp = CURRENT_TIMESTAMP
755 ))?((
756 workspace.id,
757 ssh_project.id.0,
758 workspace.docks,
759 workspace.session_id,
760 workspace.window_id
761 ))
762 .context("Updating workspace")?;
763 }
764 }
765
766 // Save center pane group
767 Self::save_pane_group(conn, workspace.id, &workspace.center_group, None)
768 .context("save pane group in save workspace")?;
769
770 Ok(())
771 })
772 .log_err();
773 })
774 .await;
775 }
776
777 pub(crate) async fn get_or_create_ssh_project(
778 &self,
779 host: String,
780 port: Option<u16>,
781 paths: Vec<String>,
782 user: Option<String>,
783 ) -> Result<SerializedSshProject> {
784 let paths = serde_json::to_string(&paths)?;
785 if let Some(project) = self
786 .get_ssh_project(host.clone(), port, paths.clone(), user.clone())
787 .await?
788 {
789 Ok(project)
790 } else {
791 self.insert_ssh_project(host, port, paths, user)
792 .await?
793 .ok_or_else(|| anyhow!("failed to insert ssh project"))
794 }
795 }
796
797 query! {
798 async fn get_ssh_project(host: String, port: Option<u16>, paths: String, user: Option<String>) -> Result<Option<SerializedSshProject>> {
799 SELECT id, host, port, paths, user
800 FROM ssh_projects
801 WHERE host IS ? AND port IS ? AND paths IS ? AND user IS ?
802 LIMIT 1
803 }
804 }
805
806 query! {
807 async fn insert_ssh_project(host: String, port: Option<u16>, paths: String, user: Option<String>) -> Result<Option<SerializedSshProject>> {
808 INSERT INTO ssh_projects(
809 host,
810 port,
811 paths,
812 user
813 ) VALUES (?1, ?2, ?3, ?4)
814 RETURNING id, host, port, paths, user
815 }
816 }
817
818 query! {
819 pub async fn next_id() -> Result<WorkspaceId> {
820 INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id
821 }
822 }
823
824 query! {
825 fn recent_workspaces() -> Result<Vec<(WorkspaceId, LocalPaths, LocalPathsOrder, Option<u64>, Option<u64>)>> {
826 SELECT workspace_id, local_paths, local_paths_order, dev_server_project_id, ssh_project_id
827 FROM workspaces
828 WHERE local_paths IS NOT NULL
829 OR dev_server_project_id IS NOT NULL
830 OR ssh_project_id IS NOT NULL
831 ORDER BY timestamp DESC
832 }
833 }
834
835 query! {
836 fn session_workspaces(session_id: String) -> Result<Vec<(LocalPaths, Option<u64>, Option<u64>)>> {
837 SELECT local_paths, window_id, ssh_project_id
838 FROM workspaces
839 WHERE session_id = ?1 AND dev_server_project_id IS NULL
840 ORDER BY timestamp DESC
841 }
842 }
843
844 query! {
845 fn dev_server_projects() -> Result<Vec<SerializedDevServerProject>> {
846 SELECT id, path, dev_server_name
847 FROM dev_server_projects
848 }
849 }
850
851 query! {
852 fn ssh_projects() -> Result<Vec<SerializedSshProject>> {
853 SELECT id, host, port, paths, user
854 FROM ssh_projects
855 }
856 }
857
858 query! {
859 fn ssh_project(id: u64) -> Result<SerializedSshProject> {
860 SELECT id, host, port, paths, user
861 FROM ssh_projects
862 WHERE id = ?
863 }
864 }
865
866 pub(crate) fn last_window(
867 &self,
868 ) -> anyhow::Result<(Option<Uuid>, Option<SerializedWindowBounds>)> {
869 let mut prepared_query =
870 self.select::<(Option<Uuid>, Option<SerializedWindowBounds>)>(sql!(
871 SELECT
872 display,
873 window_state, window_x, window_y, window_width, window_height
874 FROM workspaces
875 WHERE local_paths
876 IS NOT NULL
877 ORDER BY timestamp DESC
878 LIMIT 1
879 ))?;
880 let result = prepared_query()?;
881 Ok(result.into_iter().next().unwrap_or((None, None)))
882 }
883
884 query! {
885 pub async fn delete_workspace_by_id(id: WorkspaceId) -> Result<()> {
886 DELETE FROM workspaces
887 WHERE workspace_id IS ?
888 }
889 }
890
891 pub async fn delete_workspace_by_dev_server_project_id(
892 &self,
893 id: DevServerProjectId,
894 ) -> Result<()> {
895 self.write(move |conn| {
896 conn.exec_bound(sql!(
897 DELETE FROM dev_server_projects WHERE id = ?
898 ))?(id.0)?;
899 conn.exec_bound(sql!(
900 DELETE FROM workspaces
901 WHERE dev_server_project_id IS ?
902 ))?(id.0)
903 })
904 .await
905 }
906
907 // Returns the recent locations which are still valid on disk and deletes ones which no longer
908 // exist.
909 pub async fn recent_workspaces_on_disk(
910 &self,
911 ) -> Result<Vec<(WorkspaceId, SerializedWorkspaceLocation)>> {
912 let mut result = Vec::new();
913 let mut delete_tasks = Vec::new();
914 let dev_server_projects = self.dev_server_projects()?;
915 let ssh_projects = self.ssh_projects()?;
916
917 for (id, location, order, dev_server_project_id, ssh_project_id) in
918 self.recent_workspaces()?
919 {
920 if let Some(dev_server_project_id) = dev_server_project_id.map(DevServerProjectId) {
921 if let Some(dev_server_project) = dev_server_projects
922 .iter()
923 .find(|rp| rp.id == dev_server_project_id)
924 {
925 result.push((id, dev_server_project.clone().into()));
926 } else {
927 delete_tasks.push(self.delete_workspace_by_id(id));
928 }
929 continue;
930 }
931
932 if let Some(ssh_project_id) = ssh_project_id.map(SshProjectId) {
933 if let Some(ssh_project) = ssh_projects.iter().find(|rp| rp.id == ssh_project_id) {
934 result.push((id, SerializedWorkspaceLocation::Ssh(ssh_project.clone())));
935 } else {
936 delete_tasks.push(self.delete_workspace_by_id(id));
937 }
938 continue;
939 }
940
941 if location.paths().iter().all(|path| path.exists())
942 && location.paths().iter().any(|path| path.is_dir())
943 {
944 result.push((id, SerializedWorkspaceLocation::Local(location, order)));
945 } else {
946 delete_tasks.push(self.delete_workspace_by_id(id));
947 }
948 }
949
950 futures::future::join_all(delete_tasks).await;
951 Ok(result)
952 }
953
954 pub async fn last_workspace(&self) -> Result<Option<SerializedWorkspaceLocation>> {
955 Ok(self
956 .recent_workspaces_on_disk()
957 .await?
958 .into_iter()
959 .next()
960 .map(|(_, location)| location))
961 }
962
963 // Returns the locations of the workspaces that were still opened when the last
964 // session was closed (i.e. when Zed was quit).
965 // If `last_session_window_order` is provided, the returned locations are ordered
966 // according to that.
967 pub fn last_session_workspace_locations(
968 &self,
969 last_session_id: &str,
970 last_session_window_stack: Option<Vec<WindowId>>,
971 ) -> Result<Vec<SerializedWorkspaceLocation>> {
972 let mut workspaces = Vec::new();
973
974 for (location, window_id, ssh_project_id) in
975 self.session_workspaces(last_session_id.to_owned())?
976 {
977 if let Some(ssh_project_id) = ssh_project_id {
978 let location = SerializedWorkspaceLocation::Ssh(self.ssh_project(ssh_project_id)?);
979 workspaces.push((location, window_id.map(WindowId::from)));
980 } else if location.paths().iter().all(|path| path.exists())
981 && location.paths().iter().any(|path| path.is_dir())
982 {
983 let location =
984 SerializedWorkspaceLocation::from_local_paths(location.paths().iter());
985 workspaces.push((location, window_id.map(WindowId::from)));
986 }
987 }
988
989 if let Some(stack) = last_session_window_stack {
990 workspaces.sort_by_key(|(_, window_id)| {
991 window_id
992 .and_then(|id| stack.iter().position(|&order_id| order_id == id))
993 .unwrap_or(usize::MAX)
994 });
995 }
996
997 Ok(workspaces
998 .into_iter()
999 .map(|(paths, _)| paths)
1000 .collect::<Vec<_>>())
1001 }
1002
1003 fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
1004 Ok(self
1005 .get_pane_group(workspace_id, None)?
1006 .into_iter()
1007 .next()
1008 .unwrap_or_else(|| {
1009 SerializedPaneGroup::Pane(SerializedPane {
1010 active: true,
1011 children: vec![],
1012 pinned_count: 0,
1013 })
1014 }))
1015 }
1016
1017 fn get_pane_group(
1018 &self,
1019 workspace_id: WorkspaceId,
1020 group_id: Option<GroupId>,
1021 ) -> Result<Vec<SerializedPaneGroup>> {
1022 type GroupKey = (Option<GroupId>, WorkspaceId);
1023 type GroupOrPane = (
1024 Option<GroupId>,
1025 Option<SerializedAxis>,
1026 Option<PaneId>,
1027 Option<bool>,
1028 Option<usize>,
1029 Option<String>,
1030 );
1031 self.select_bound::<GroupKey, GroupOrPane>(sql!(
1032 SELECT group_id, axis, pane_id, active, pinned_count, flexes
1033 FROM (SELECT
1034 group_id,
1035 axis,
1036 NULL as pane_id,
1037 NULL as active,
1038 NULL as pinned_count,
1039 position,
1040 parent_group_id,
1041 workspace_id,
1042 flexes
1043 FROM pane_groups
1044 UNION
1045 SELECT
1046 NULL,
1047 NULL,
1048 center_panes.pane_id,
1049 panes.active as active,
1050 pinned_count,
1051 position,
1052 parent_group_id,
1053 panes.workspace_id as workspace_id,
1054 NULL
1055 FROM center_panes
1056 JOIN panes ON center_panes.pane_id = panes.pane_id)
1057 WHERE parent_group_id IS ? AND workspace_id = ?
1058 ORDER BY position
1059 ))?((group_id, workspace_id))?
1060 .into_iter()
1061 .map(|(group_id, axis, pane_id, active, pinned_count, flexes)| {
1062 let maybe_pane = maybe!({ Some((pane_id?, active?, pinned_count?)) });
1063 if let Some((group_id, axis)) = group_id.zip(axis) {
1064 let flexes = flexes
1065 .map(|flexes: String| serde_json::from_str::<Vec<f32>>(&flexes))
1066 .transpose()?;
1067
1068 Ok(SerializedPaneGroup::Group {
1069 axis,
1070 children: self.get_pane_group(workspace_id, Some(group_id))?,
1071 flexes,
1072 })
1073 } else if let Some((pane_id, active, pinned_count)) = maybe_pane {
1074 Ok(SerializedPaneGroup::Pane(SerializedPane::new(
1075 self.get_items(pane_id)?,
1076 active,
1077 pinned_count,
1078 )))
1079 } else {
1080 bail!("Pane Group Child was neither a pane group or a pane");
1081 }
1082 })
1083 // Filter out panes and pane groups which don't have any children or items
1084 .filter(|pane_group| match pane_group {
1085 Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
1086 Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
1087 _ => true,
1088 })
1089 .collect::<Result<_>>()
1090 }
1091
1092 fn save_pane_group(
1093 conn: &Connection,
1094 workspace_id: WorkspaceId,
1095 pane_group: &SerializedPaneGroup,
1096 parent: Option<(GroupId, usize)>,
1097 ) -> Result<()> {
1098 match pane_group {
1099 SerializedPaneGroup::Group {
1100 axis,
1101 children,
1102 flexes,
1103 } => {
1104 let (parent_id, position) = parent.unzip();
1105
1106 let flex_string = flexes
1107 .as_ref()
1108 .map(|flexes| serde_json::json!(flexes).to_string());
1109
1110 let group_id = conn.select_row_bound::<_, i64>(sql!(
1111 INSERT INTO pane_groups(
1112 workspace_id,
1113 parent_group_id,
1114 position,
1115 axis,
1116 flexes
1117 )
1118 VALUES (?, ?, ?, ?, ?)
1119 RETURNING group_id
1120 ))?((
1121 workspace_id,
1122 parent_id,
1123 position,
1124 *axis,
1125 flex_string,
1126 ))?
1127 .ok_or_else(|| anyhow!("Couldn't retrieve group_id from inserted pane_group"))?;
1128
1129 for (position, group) in children.iter().enumerate() {
1130 Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))?
1131 }
1132
1133 Ok(())
1134 }
1135 SerializedPaneGroup::Pane(pane) => {
1136 Self::save_pane(conn, workspace_id, pane, parent)?;
1137 Ok(())
1138 }
1139 }
1140 }
1141
1142 fn save_pane(
1143 conn: &Connection,
1144 workspace_id: WorkspaceId,
1145 pane: &SerializedPane,
1146 parent: Option<(GroupId, usize)>,
1147 ) -> Result<PaneId> {
1148 let pane_id = conn.select_row_bound::<_, i64>(sql!(
1149 INSERT INTO panes(workspace_id, active, pinned_count)
1150 VALUES (?, ?, ?)
1151 RETURNING pane_id
1152 ))?((workspace_id, pane.active, pane.pinned_count))?
1153 .ok_or_else(|| anyhow!("Could not retrieve inserted pane_id"))?;
1154
1155 let (parent_id, order) = parent.unzip();
1156 conn.exec_bound(sql!(
1157 INSERT INTO center_panes(pane_id, parent_group_id, position)
1158 VALUES (?, ?, ?)
1159 ))?((pane_id, parent_id, order))?;
1160
1161 Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
1162
1163 Ok(pane_id)
1164 }
1165
1166 fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
1167 self.select_bound(sql!(
1168 SELECT kind, item_id, active, preview FROM items
1169 WHERE pane_id = ?
1170 ORDER BY position
1171 ))?(pane_id)
1172 }
1173
1174 fn save_items(
1175 conn: &Connection,
1176 workspace_id: WorkspaceId,
1177 pane_id: PaneId,
1178 items: &[SerializedItem],
1179 ) -> Result<()> {
1180 let mut insert = conn.exec_bound(sql!(
1181 INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active, preview) VALUES (?, ?, ?, ?, ?, ?, ?)
1182 )).context("Preparing insertion")?;
1183 for (position, item) in items.iter().enumerate() {
1184 insert((workspace_id, pane_id, position, item))?;
1185 }
1186
1187 Ok(())
1188 }
1189
1190 query! {
1191 pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> {
1192 UPDATE workspaces
1193 SET timestamp = CURRENT_TIMESTAMP
1194 WHERE workspace_id = ?
1195 }
1196 }
1197
1198 query! {
1199 pub(crate) async fn set_window_open_status(workspace_id: WorkspaceId, bounds: SerializedWindowBounds, display: Uuid) -> Result<()> {
1200 UPDATE workspaces
1201 SET window_state = ?2,
1202 window_x = ?3,
1203 window_y = ?4,
1204 window_width = ?5,
1205 window_height = ?6,
1206 display = ?7
1207 WHERE workspace_id = ?1
1208 }
1209 }
1210
1211 query! {
1212 pub(crate) async fn set_centered_layout(workspace_id: WorkspaceId, centered_layout: bool) -> Result<()> {
1213 UPDATE workspaces
1214 SET centered_layout = ?2
1215 WHERE workspace_id = ?1
1216 }
1217 }
1218}
1219
1220#[cfg(test)]
1221mod tests {
1222 use super::*;
1223 use crate::persistence::model::SerializedWorkspace;
1224 use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
1225 use db::open_test_db;
1226 use gpui::{self};
1227
1228 #[gpui::test]
1229 async fn test_next_id_stability() {
1230 env_logger::try_init().ok();
1231
1232 let db = WorkspaceDb(open_test_db("test_next_id_stability").await);
1233
1234 db.write(|conn| {
1235 conn.migrate(
1236 "test_table",
1237 &[sql!(
1238 CREATE TABLE test_table(
1239 text TEXT,
1240 workspace_id INTEGER,
1241 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
1242 ON DELETE CASCADE
1243 ) STRICT;
1244 )],
1245 )
1246 .unwrap();
1247 })
1248 .await;
1249
1250 let id = db.next_id().await.unwrap();
1251 // Assert the empty row got inserted
1252 assert_eq!(
1253 Some(id),
1254 db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
1255 SELECT workspace_id FROM workspaces WHERE workspace_id = ?
1256 ))
1257 .unwrap()(id)
1258 .unwrap()
1259 );
1260
1261 db.write(move |conn| {
1262 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
1263 .unwrap()(("test-text-1", id))
1264 .unwrap()
1265 })
1266 .await;
1267
1268 let test_text_1 = db
1269 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
1270 .unwrap()(1)
1271 .unwrap()
1272 .unwrap();
1273 assert_eq!(test_text_1, "test-text-1");
1274 }
1275
1276 #[gpui::test]
1277 async fn test_workspace_id_stability() {
1278 env_logger::try_init().ok();
1279
1280 let db = WorkspaceDb(open_test_db("test_workspace_id_stability").await);
1281
1282 db.write(|conn| {
1283 conn.migrate(
1284 "test_table",
1285 &[sql!(
1286 CREATE TABLE test_table(
1287 text TEXT,
1288 workspace_id INTEGER,
1289 FOREIGN KEY(workspace_id)
1290 REFERENCES workspaces(workspace_id)
1291 ON DELETE CASCADE
1292 ) STRICT;)],
1293 )
1294 })
1295 .await
1296 .unwrap();
1297
1298 let mut workspace_1 = SerializedWorkspace {
1299 id: WorkspaceId(1),
1300 location: SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp2"]),
1301 center_group: Default::default(),
1302 window_bounds: Default::default(),
1303 display: Default::default(),
1304 docks: Default::default(),
1305 centered_layout: false,
1306 session_id: None,
1307 window_id: None,
1308 };
1309
1310 let workspace_2 = SerializedWorkspace {
1311 id: WorkspaceId(2),
1312 location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]),
1313 center_group: Default::default(),
1314 window_bounds: Default::default(),
1315 display: Default::default(),
1316 docks: Default::default(),
1317 centered_layout: false,
1318 session_id: None,
1319 window_id: None,
1320 };
1321
1322 db.save_workspace(workspace_1.clone()).await;
1323
1324 db.write(|conn| {
1325 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
1326 .unwrap()(("test-text-1", 1))
1327 .unwrap();
1328 })
1329 .await;
1330
1331 db.save_workspace(workspace_2.clone()).await;
1332
1333 db.write(|conn| {
1334 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
1335 .unwrap()(("test-text-2", 2))
1336 .unwrap();
1337 })
1338 .await;
1339
1340 workspace_1.location = SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp3"]);
1341 db.save_workspace(workspace_1.clone()).await;
1342 db.save_workspace(workspace_1).await;
1343 db.save_workspace(workspace_2).await;
1344
1345 let test_text_2 = db
1346 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
1347 .unwrap()(2)
1348 .unwrap()
1349 .unwrap();
1350 assert_eq!(test_text_2, "test-text-2");
1351
1352 let test_text_1 = db
1353 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
1354 .unwrap()(1)
1355 .unwrap()
1356 .unwrap();
1357 assert_eq!(test_text_1, "test-text-1");
1358 }
1359
1360 fn group(axis: Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
1361 SerializedPaneGroup::Group {
1362 axis: SerializedAxis(axis),
1363 flexes: None,
1364 children,
1365 }
1366 }
1367
1368 #[gpui::test]
1369 async fn test_full_workspace_serialization() {
1370 env_logger::try_init().ok();
1371
1372 let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await);
1373
1374 // -----------------
1375 // | 1,2 | 5,6 |
1376 // | - - - | |
1377 // | 3,4 | |
1378 // -----------------
1379 let center_group = group(
1380 Axis::Horizontal,
1381 vec![
1382 group(
1383 Axis::Vertical,
1384 vec![
1385 SerializedPaneGroup::Pane(SerializedPane::new(
1386 vec![
1387 SerializedItem::new("Terminal", 5, false, false),
1388 SerializedItem::new("Terminal", 6, true, false),
1389 ],
1390 false,
1391 0,
1392 )),
1393 SerializedPaneGroup::Pane(SerializedPane::new(
1394 vec![
1395 SerializedItem::new("Terminal", 7, true, false),
1396 SerializedItem::new("Terminal", 8, false, false),
1397 ],
1398 false,
1399 0,
1400 )),
1401 ],
1402 ),
1403 SerializedPaneGroup::Pane(SerializedPane::new(
1404 vec![
1405 SerializedItem::new("Terminal", 9, false, false),
1406 SerializedItem::new("Terminal", 10, true, false),
1407 ],
1408 false,
1409 0,
1410 )),
1411 ],
1412 );
1413
1414 let workspace = SerializedWorkspace {
1415 id: WorkspaceId(5),
1416 location: SerializedWorkspaceLocation::Local(
1417 LocalPaths::new(["/tmp", "/tmp2"]),
1418 LocalPathsOrder::new([1, 0]),
1419 ),
1420 center_group,
1421 window_bounds: Default::default(),
1422 display: Default::default(),
1423 docks: Default::default(),
1424 centered_layout: false,
1425 session_id: None,
1426 window_id: Some(999),
1427 };
1428
1429 db.save_workspace(workspace.clone()).await;
1430
1431 let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
1432 assert_eq!(workspace, round_trip_workspace.unwrap());
1433
1434 // Test guaranteed duplicate IDs
1435 db.save_workspace(workspace.clone()).await;
1436 db.save_workspace(workspace.clone()).await;
1437
1438 let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
1439 assert_eq!(workspace, round_trip_workspace.unwrap());
1440 }
1441
1442 #[gpui::test]
1443 async fn test_workspace_assignment() {
1444 env_logger::try_init().ok();
1445
1446 let db = WorkspaceDb(open_test_db("test_basic_functionality").await);
1447
1448 let workspace_1 = SerializedWorkspace {
1449 id: WorkspaceId(1),
1450 location: SerializedWorkspaceLocation::Local(
1451 LocalPaths::new(["/tmp", "/tmp2"]),
1452 LocalPathsOrder::new([0, 1]),
1453 ),
1454 center_group: Default::default(),
1455 window_bounds: Default::default(),
1456 display: Default::default(),
1457 docks: Default::default(),
1458 centered_layout: false,
1459 session_id: None,
1460 window_id: Some(1),
1461 };
1462
1463 let mut workspace_2 = SerializedWorkspace {
1464 id: WorkspaceId(2),
1465 location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]),
1466 center_group: Default::default(),
1467 window_bounds: Default::default(),
1468 display: Default::default(),
1469 docks: Default::default(),
1470 centered_layout: false,
1471 session_id: None,
1472 window_id: Some(2),
1473 };
1474
1475 db.save_workspace(workspace_1.clone()).await;
1476 db.save_workspace(workspace_2.clone()).await;
1477
1478 // Test that paths are treated as a set
1479 assert_eq!(
1480 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
1481 workspace_1
1482 );
1483 assert_eq!(
1484 db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
1485 workspace_1
1486 );
1487
1488 // Make sure that other keys work
1489 assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
1490 assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
1491
1492 // Test 'mutate' case of updating a pre-existing id
1493 workspace_2.location = SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp2"]);
1494
1495 db.save_workspace(workspace_2.clone()).await;
1496 assert_eq!(
1497 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
1498 workspace_2
1499 );
1500
1501 // Test other mechanism for mutating
1502 let mut workspace_3 = SerializedWorkspace {
1503 id: WorkspaceId(3),
1504 location: SerializedWorkspaceLocation::Local(
1505 LocalPaths::new(["/tmp", "/tmp2"]),
1506 LocalPathsOrder::new([1, 0]),
1507 ),
1508 center_group: Default::default(),
1509 window_bounds: Default::default(),
1510 display: Default::default(),
1511 docks: Default::default(),
1512 centered_layout: false,
1513 session_id: None,
1514 window_id: Some(3),
1515 };
1516
1517 db.save_workspace(workspace_3.clone()).await;
1518 assert_eq!(
1519 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
1520 workspace_3
1521 );
1522
1523 // Make sure that updating paths differently also works
1524 workspace_3.location =
1525 SerializedWorkspaceLocation::from_local_paths(["/tmp3", "/tmp4", "/tmp2"]);
1526 db.save_workspace(workspace_3.clone()).await;
1527 assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
1528 assert_eq!(
1529 db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
1530 .unwrap(),
1531 workspace_3
1532 );
1533 }
1534
1535 #[gpui::test]
1536 async fn test_session_workspaces() {
1537 env_logger::try_init().ok();
1538
1539 let db = WorkspaceDb(open_test_db("test_serializing_workspaces_session_id").await);
1540
1541 let workspace_1 = SerializedWorkspace {
1542 id: WorkspaceId(1),
1543 location: SerializedWorkspaceLocation::from_local_paths(["/tmp1"]),
1544 center_group: Default::default(),
1545 window_bounds: Default::default(),
1546 display: Default::default(),
1547 docks: Default::default(),
1548 centered_layout: false,
1549 session_id: Some("session-id-1".to_owned()),
1550 window_id: Some(10),
1551 };
1552
1553 let workspace_2 = SerializedWorkspace {
1554 id: WorkspaceId(2),
1555 location: SerializedWorkspaceLocation::from_local_paths(["/tmp2"]),
1556 center_group: Default::default(),
1557 window_bounds: Default::default(),
1558 display: Default::default(),
1559 docks: Default::default(),
1560 centered_layout: false,
1561 session_id: Some("session-id-1".to_owned()),
1562 window_id: Some(20),
1563 };
1564
1565 let workspace_3 = SerializedWorkspace {
1566 id: WorkspaceId(3),
1567 location: SerializedWorkspaceLocation::from_local_paths(["/tmp3"]),
1568 center_group: Default::default(),
1569 window_bounds: Default::default(),
1570 display: Default::default(),
1571 docks: Default::default(),
1572 centered_layout: false,
1573 session_id: Some("session-id-2".to_owned()),
1574 window_id: Some(30),
1575 };
1576
1577 let workspace_4 = SerializedWorkspace {
1578 id: WorkspaceId(4),
1579 location: SerializedWorkspaceLocation::from_local_paths(["/tmp4"]),
1580 center_group: Default::default(),
1581 window_bounds: Default::default(),
1582 display: Default::default(),
1583 docks: Default::default(),
1584 centered_layout: false,
1585 session_id: None,
1586 window_id: None,
1587 };
1588
1589 let ssh_project = db
1590 .get_or_create_ssh_project("my-host".to_string(), Some(1234), vec![], None)
1591 .await
1592 .unwrap();
1593
1594 let workspace_5 = SerializedWorkspace {
1595 id: WorkspaceId(5),
1596 location: SerializedWorkspaceLocation::Ssh(ssh_project.clone()),
1597 center_group: Default::default(),
1598 window_bounds: Default::default(),
1599 display: Default::default(),
1600 docks: Default::default(),
1601 centered_layout: false,
1602 session_id: Some("session-id-2".to_owned()),
1603 window_id: Some(50),
1604 };
1605
1606 db.save_workspace(workspace_1.clone()).await;
1607 db.save_workspace(workspace_2.clone()).await;
1608 db.save_workspace(workspace_3.clone()).await;
1609 db.save_workspace(workspace_4.clone()).await;
1610 db.save_workspace(workspace_5.clone()).await;
1611
1612 let locations = db.session_workspaces("session-id-1".to_owned()).unwrap();
1613 assert_eq!(locations.len(), 2);
1614 assert_eq!(locations[0].0, LocalPaths::new(["/tmp1"]));
1615 assert_eq!(locations[0].1, Some(10));
1616 assert_eq!(locations[1].0, LocalPaths::new(["/tmp2"]));
1617 assert_eq!(locations[1].1, Some(20));
1618
1619 let locations = db.session_workspaces("session-id-2".to_owned()).unwrap();
1620 assert_eq!(locations.len(), 2);
1621 assert_eq!(locations[0].0, LocalPaths::new(["/tmp3"]));
1622 assert_eq!(locations[0].1, Some(30));
1623 let empty_paths: Vec<&str> = Vec::new();
1624 assert_eq!(locations[1].0, LocalPaths::new(empty_paths.iter()));
1625 assert_eq!(locations[1].1, Some(50));
1626 assert_eq!(locations[1].2, Some(ssh_project.id.0));
1627 }
1628
1629 fn default_workspace<P: AsRef<Path>>(
1630 workspace_id: &[P],
1631 center_group: &SerializedPaneGroup,
1632 ) -> SerializedWorkspace {
1633 SerializedWorkspace {
1634 id: WorkspaceId(4),
1635 location: SerializedWorkspaceLocation::from_local_paths(workspace_id),
1636 center_group: center_group.clone(),
1637 window_bounds: Default::default(),
1638 display: Default::default(),
1639 docks: Default::default(),
1640 centered_layout: false,
1641 session_id: None,
1642 window_id: None,
1643 }
1644 }
1645
1646 #[gpui::test]
1647 async fn test_last_session_workspace_locations() {
1648 let dir1 = tempfile::TempDir::with_prefix("dir1").unwrap();
1649 let dir2 = tempfile::TempDir::with_prefix("dir2").unwrap();
1650 let dir3 = tempfile::TempDir::with_prefix("dir3").unwrap();
1651 let dir4 = tempfile::TempDir::with_prefix("dir4").unwrap();
1652
1653 let db =
1654 WorkspaceDb(open_test_db("test_serializing_workspaces_last_session_workspaces").await);
1655
1656 let workspaces = [
1657 (1, dir1.path().to_str().unwrap(), 9),
1658 (2, dir2.path().to_str().unwrap(), 5),
1659 (3, dir3.path().to_str().unwrap(), 8),
1660 (4, dir4.path().to_str().unwrap(), 2),
1661 ]
1662 .into_iter()
1663 .map(|(id, location, window_id)| SerializedWorkspace {
1664 id: WorkspaceId(id),
1665 location: SerializedWorkspaceLocation::from_local_paths([location]),
1666 center_group: Default::default(),
1667 window_bounds: Default::default(),
1668 display: Default::default(),
1669 docks: Default::default(),
1670 centered_layout: false,
1671 session_id: Some("one-session".to_owned()),
1672 window_id: Some(window_id),
1673 })
1674 .collect::<Vec<_>>();
1675
1676 for workspace in workspaces.iter() {
1677 db.save_workspace(workspace.clone()).await;
1678 }
1679
1680 let stack = Some(Vec::from([
1681 WindowId::from(2), // Top
1682 WindowId::from(8),
1683 WindowId::from(5),
1684 WindowId::from(9), // Bottom
1685 ]));
1686
1687 let have = db
1688 .last_session_workspace_locations("one-session", stack)
1689 .unwrap();
1690 assert_eq!(have.len(), 4);
1691 assert_eq!(
1692 have[0],
1693 SerializedWorkspaceLocation::from_local_paths(&[dir4.path().to_str().unwrap()])
1694 );
1695 assert_eq!(
1696 have[1],
1697 SerializedWorkspaceLocation::from_local_paths([dir3.path().to_str().unwrap()])
1698 );
1699 assert_eq!(
1700 have[2],
1701 SerializedWorkspaceLocation::from_local_paths([dir2.path().to_str().unwrap()])
1702 );
1703 assert_eq!(
1704 have[3],
1705 SerializedWorkspaceLocation::from_local_paths([dir1.path().to_str().unwrap()])
1706 );
1707 }
1708
1709 #[gpui::test]
1710 async fn test_last_session_workspace_locations_ssh_projects() {
1711 let db = WorkspaceDb(
1712 open_test_db("test_serializing_workspaces_last_session_workspaces_ssh_projects").await,
1713 );
1714
1715 let ssh_projects = [
1716 ("host-1", "my-user-1"),
1717 ("host-2", "my-user-2"),
1718 ("host-3", "my-user-3"),
1719 ("host-4", "my-user-4"),
1720 ]
1721 .into_iter()
1722 .map(|(host, user)| async {
1723 db.get_or_create_ssh_project(host.to_string(), None, vec![], Some(user.to_string()))
1724 .await
1725 .unwrap()
1726 })
1727 .collect::<Vec<_>>();
1728
1729 let ssh_projects = futures::future::join_all(ssh_projects).await;
1730
1731 let workspaces = [
1732 (1, ssh_projects[0].clone(), 9),
1733 (2, ssh_projects[1].clone(), 5),
1734 (3, ssh_projects[2].clone(), 8),
1735 (4, ssh_projects[3].clone(), 2),
1736 ]
1737 .into_iter()
1738 .map(|(id, ssh_project, window_id)| SerializedWorkspace {
1739 id: WorkspaceId(id),
1740 location: SerializedWorkspaceLocation::Ssh(ssh_project),
1741 center_group: Default::default(),
1742 window_bounds: Default::default(),
1743 display: Default::default(),
1744 docks: Default::default(),
1745 centered_layout: false,
1746 session_id: Some("one-session".to_owned()),
1747 window_id: Some(window_id),
1748 })
1749 .collect::<Vec<_>>();
1750
1751 for workspace in workspaces.iter() {
1752 db.save_workspace(workspace.clone()).await;
1753 }
1754
1755 let stack = Some(Vec::from([
1756 WindowId::from(2), // Top
1757 WindowId::from(8),
1758 WindowId::from(5),
1759 WindowId::from(9), // Bottom
1760 ]));
1761
1762 let have = db
1763 .last_session_workspace_locations("one-session", stack)
1764 .unwrap();
1765 assert_eq!(have.len(), 4);
1766 assert_eq!(
1767 have[0],
1768 SerializedWorkspaceLocation::Ssh(ssh_projects[3].clone())
1769 );
1770 assert_eq!(
1771 have[1],
1772 SerializedWorkspaceLocation::Ssh(ssh_projects[2].clone())
1773 );
1774 assert_eq!(
1775 have[2],
1776 SerializedWorkspaceLocation::Ssh(ssh_projects[1].clone())
1777 );
1778 assert_eq!(
1779 have[3],
1780 SerializedWorkspaceLocation::Ssh(ssh_projects[0].clone())
1781 );
1782 }
1783
1784 #[gpui::test]
1785 async fn test_get_or_create_ssh_project() {
1786 let db = WorkspaceDb(open_test_db("test_get_or_create_ssh_project").await);
1787
1788 let (host, port, paths, user) = (
1789 "example.com".to_string(),
1790 Some(22_u16),
1791 vec!["/home/user".to_string(), "/etc/nginx".to_string()],
1792 Some("user".to_string()),
1793 );
1794
1795 let project = db
1796 .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone())
1797 .await
1798 .unwrap();
1799
1800 assert_eq!(project.host, host);
1801 assert_eq!(project.paths, paths);
1802 assert_eq!(project.user, user);
1803
1804 // Test that calling the function again with the same parameters returns the same project
1805 let same_project = db
1806 .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone())
1807 .await
1808 .unwrap();
1809
1810 assert_eq!(project.id, same_project.id);
1811
1812 // Test with different parameters
1813 let (host2, paths2, user2) = (
1814 "otherexample.com".to_string(),
1815 vec!["/home/otheruser".to_string()],
1816 Some("otheruser".to_string()),
1817 );
1818
1819 let different_project = db
1820 .get_or_create_ssh_project(host2.clone(), None, paths2.clone(), user2.clone())
1821 .await
1822 .unwrap();
1823
1824 assert_ne!(project.id, different_project.id);
1825 assert_eq!(different_project.host, host2);
1826 assert_eq!(different_project.paths, paths2);
1827 assert_eq!(different_project.user, user2);
1828 }
1829
1830 #[gpui::test]
1831 async fn test_get_or_create_ssh_project_with_null_user() {
1832 let db = WorkspaceDb(open_test_db("test_get_or_create_ssh_project_with_null_user").await);
1833
1834 let (host, port, paths, user) = (
1835 "example.com".to_string(),
1836 None,
1837 vec!["/home/user".to_string()],
1838 None,
1839 );
1840
1841 let project = db
1842 .get_or_create_ssh_project(host.clone(), port, paths.clone(), None)
1843 .await
1844 .unwrap();
1845
1846 assert_eq!(project.host, host);
1847 assert_eq!(project.paths, paths);
1848 assert_eq!(project.user, None);
1849
1850 // Test that calling the function again with the same parameters returns the same project
1851 let same_project = db
1852 .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone())
1853 .await
1854 .unwrap();
1855
1856 assert_eq!(project.id, same_project.id);
1857 }
1858
1859 #[gpui::test]
1860 async fn test_get_ssh_projects() {
1861 let db = WorkspaceDb(open_test_db("test_get_ssh_projects").await);
1862
1863 let projects = vec![
1864 (
1865 "example.com".to_string(),
1866 None,
1867 vec!["/home/user".to_string()],
1868 None,
1869 ),
1870 (
1871 "anotherexample.com".to_string(),
1872 Some(123_u16),
1873 vec!["/home/user2".to_string()],
1874 Some("user2".to_string()),
1875 ),
1876 (
1877 "yetanother.com".to_string(),
1878 Some(345_u16),
1879 vec!["/home/user3".to_string(), "/proc/1234/exe".to_string()],
1880 None,
1881 ),
1882 ];
1883
1884 for (host, port, paths, user) in projects.iter() {
1885 let project = db
1886 .get_or_create_ssh_project(host.clone(), *port, paths.clone(), user.clone())
1887 .await
1888 .unwrap();
1889
1890 assert_eq!(&project.host, host);
1891 assert_eq!(&project.port, port);
1892 assert_eq!(&project.paths, paths);
1893 assert_eq!(&project.user, user);
1894 }
1895
1896 let stored_projects = db.ssh_projects().unwrap();
1897 assert_eq!(stored_projects.len(), projects.len());
1898 }
1899
1900 #[gpui::test]
1901 async fn test_simple_split() {
1902 env_logger::try_init().ok();
1903
1904 let db = WorkspaceDb(open_test_db("simple_split").await);
1905
1906 // -----------------
1907 // | 1,2 | 5,6 |
1908 // | - - - | |
1909 // | 3,4 | |
1910 // -----------------
1911 let center_pane = group(
1912 Axis::Horizontal,
1913 vec![
1914 group(
1915 Axis::Vertical,
1916 vec![
1917 SerializedPaneGroup::Pane(SerializedPane::new(
1918 vec![
1919 SerializedItem::new("Terminal", 1, false, false),
1920 SerializedItem::new("Terminal", 2, true, false),
1921 ],
1922 false,
1923 0,
1924 )),
1925 SerializedPaneGroup::Pane(SerializedPane::new(
1926 vec![
1927 SerializedItem::new("Terminal", 4, false, false),
1928 SerializedItem::new("Terminal", 3, true, false),
1929 ],
1930 true,
1931 0,
1932 )),
1933 ],
1934 ),
1935 SerializedPaneGroup::Pane(SerializedPane::new(
1936 vec![
1937 SerializedItem::new("Terminal", 5, true, false),
1938 SerializedItem::new("Terminal", 6, false, false),
1939 ],
1940 false,
1941 0,
1942 )),
1943 ],
1944 );
1945
1946 let workspace = default_workspace(&["/tmp"], ¢er_pane);
1947
1948 db.save_workspace(workspace.clone()).await;
1949
1950 let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
1951
1952 assert_eq!(workspace.center_group, new_workspace.center_group);
1953 }
1954
1955 #[gpui::test]
1956 async fn test_cleanup_panes() {
1957 env_logger::try_init().ok();
1958
1959 let db = WorkspaceDb(open_test_db("test_cleanup_panes").await);
1960
1961 let center_pane = group(
1962 Axis::Horizontal,
1963 vec![
1964 group(
1965 Axis::Vertical,
1966 vec![
1967 SerializedPaneGroup::Pane(SerializedPane::new(
1968 vec![
1969 SerializedItem::new("Terminal", 1, false, false),
1970 SerializedItem::new("Terminal", 2, true, false),
1971 ],
1972 false,
1973 0,
1974 )),
1975 SerializedPaneGroup::Pane(SerializedPane::new(
1976 vec![
1977 SerializedItem::new("Terminal", 4, false, false),
1978 SerializedItem::new("Terminal", 3, true, false),
1979 ],
1980 true,
1981 0,
1982 )),
1983 ],
1984 ),
1985 SerializedPaneGroup::Pane(SerializedPane::new(
1986 vec![
1987 SerializedItem::new("Terminal", 5, false, false),
1988 SerializedItem::new("Terminal", 6, true, false),
1989 ],
1990 false,
1991 0,
1992 )),
1993 ],
1994 );
1995
1996 let id = &["/tmp"];
1997
1998 let mut workspace = default_workspace(id, ¢er_pane);
1999
2000 db.save_workspace(workspace.clone()).await;
2001
2002 workspace.center_group = group(
2003 Axis::Vertical,
2004 vec![
2005 SerializedPaneGroup::Pane(SerializedPane::new(
2006 vec![
2007 SerializedItem::new("Terminal", 1, false, false),
2008 SerializedItem::new("Terminal", 2, true, false),
2009 ],
2010 false,
2011 0,
2012 )),
2013 SerializedPaneGroup::Pane(SerializedPane::new(
2014 vec![
2015 SerializedItem::new("Terminal", 4, true, false),
2016 SerializedItem::new("Terminal", 3, false, false),
2017 ],
2018 true,
2019 0,
2020 )),
2021 ],
2022 );
2023
2024 db.save_workspace(workspace.clone()).await;
2025
2026 let new_workspace = db.workspace_for_roots(id).unwrap();
2027
2028 assert_eq!(workspace.center_group, new_workspace.center_group);
2029 }
2030}