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