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