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