1pub mod model;
2
3use std::{
4 borrow::Cow,
5 collections::BTreeMap,
6 path::{Path, PathBuf},
7 str::FromStr,
8 sync::Arc,
9};
10
11use anyhow::{Context as _, Result, bail};
12use collections::{HashMap, IndexSet};
13use db::{
14 query,
15 sqlez::{connection::Connection, domain::Domain},
16 sqlez_macros::sql,
17};
18use gpui::{Axis, Bounds, Task, WindowBounds, WindowId, point, size};
19use project::debugger::breakpoint_store::{BreakpointState, SourceBreakpoint};
20
21use language::{LanguageName, Toolchain, ToolchainScope};
22use project::WorktreeId;
23use remote::{RemoteConnectionOptions, SshConnectionOptions, WslConnectionOptions};
24use sqlez::{
25 bindable::{Bind, Column, StaticColumnCount},
26 statement::Statement,
27 thread_safe_connection::ThreadSafeConnection,
28};
29
30use ui::{App, SharedString, px};
31use util::{ResultExt, maybe};
32use uuid::Uuid;
33
34use crate::{
35 WorkspaceId,
36 path_list::{PathList, SerializedPathList},
37 persistence::model::RemoteConnectionKind,
38};
39
40use model::{
41 GroupId, ItemId, PaneId, RemoteConnectionId, SerializedItem, SerializedPane,
42 SerializedPaneGroup, SerializedWorkspace,
43};
44
45use self::model::{DockStructure, SerializedWorkspaceLocation};
46
47#[derive(Copy, Clone, Debug, PartialEq)]
48pub(crate) struct SerializedAxis(pub(crate) gpui::Axis);
49impl sqlez::bindable::StaticColumnCount for SerializedAxis {}
50impl sqlez::bindable::Bind for SerializedAxis {
51 fn bind(
52 &self,
53 statement: &sqlez::statement::Statement,
54 start_index: i32,
55 ) -> anyhow::Result<i32> {
56 match self.0 {
57 gpui::Axis::Horizontal => "Horizontal",
58 gpui::Axis::Vertical => "Vertical",
59 }
60 .bind(statement, start_index)
61 }
62}
63
64impl sqlez::bindable::Column for SerializedAxis {
65 fn column(
66 statement: &mut sqlez::statement::Statement,
67 start_index: i32,
68 ) -> anyhow::Result<(Self, i32)> {
69 String::column(statement, start_index).and_then(|(axis_text, next_index)| {
70 Ok((
71 match axis_text.as_str() {
72 "Horizontal" => Self(Axis::Horizontal),
73 "Vertical" => Self(Axis::Vertical),
74 _ => anyhow::bail!("Stored serialized item kind is incorrect"),
75 },
76 next_index,
77 ))
78 })
79 }
80}
81
82#[derive(Copy, Clone, Debug, PartialEq, Default)]
83pub(crate) struct SerializedWindowBounds(pub(crate) WindowBounds);
84
85impl StaticColumnCount for SerializedWindowBounds {
86 fn column_count() -> usize {
87 5
88 }
89}
90
91impl Bind for SerializedWindowBounds {
92 fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
93 match self.0 {
94 WindowBounds::Windowed(bounds) => {
95 let next_index = statement.bind(&"Windowed", start_index)?;
96 statement.bind(
97 &(
98 SerializedPixels(bounds.origin.x),
99 SerializedPixels(bounds.origin.y),
100 SerializedPixels(bounds.size.width),
101 SerializedPixels(bounds.size.height),
102 ),
103 next_index,
104 )
105 }
106 WindowBounds::Maximized(bounds) => {
107 let next_index = statement.bind(&"Maximized", start_index)?;
108 statement.bind(
109 &(
110 SerializedPixels(bounds.origin.x),
111 SerializedPixels(bounds.origin.y),
112 SerializedPixels(bounds.size.width),
113 SerializedPixels(bounds.size.height),
114 ),
115 next_index,
116 )
117 }
118 WindowBounds::Fullscreen(bounds) => {
119 let next_index = statement.bind(&"FullScreen", start_index)?;
120 statement.bind(
121 &(
122 SerializedPixels(bounds.origin.x),
123 SerializedPixels(bounds.origin.y),
124 SerializedPixels(bounds.size.width),
125 SerializedPixels(bounds.size.height),
126 ),
127 next_index,
128 )
129 }
130 }
131 }
132}
133
134impl Column for SerializedWindowBounds {
135 fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
136 let (window_state, next_index) = String::column(statement, start_index)?;
137 let ((x, y, width, height), _): ((i32, i32, i32, i32), _) =
138 Column::column(statement, next_index)?;
139 let bounds = Bounds {
140 origin: point(px(x as f32), px(y as f32)),
141 size: size(px(width as f32), px(height as f32)),
142 };
143
144 let status = match window_state.as_str() {
145 "Windowed" | "Fixed" => SerializedWindowBounds(WindowBounds::Windowed(bounds)),
146 "Maximized" => SerializedWindowBounds(WindowBounds::Maximized(bounds)),
147 "FullScreen" => SerializedWindowBounds(WindowBounds::Fullscreen(bounds)),
148 _ => bail!("Window State did not have a valid string"),
149 };
150
151 Ok((status, next_index + 4))
152 }
153}
154
155#[derive(Debug)]
156pub struct Breakpoint {
157 pub position: u32,
158 pub message: Option<Arc<str>>,
159 pub condition: Option<Arc<str>>,
160 pub hit_condition: Option<Arc<str>>,
161 pub state: BreakpointState,
162}
163
164/// Wrapper for DB type of a breakpoint
165struct BreakpointStateWrapper<'a>(Cow<'a, BreakpointState>);
166
167impl From<BreakpointState> for BreakpointStateWrapper<'static> {
168 fn from(kind: BreakpointState) -> Self {
169 BreakpointStateWrapper(Cow::Owned(kind))
170 }
171}
172
173impl StaticColumnCount for BreakpointStateWrapper<'_> {
174 fn column_count() -> usize {
175 1
176 }
177}
178
179impl Bind for BreakpointStateWrapper<'_> {
180 fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result<i32> {
181 statement.bind(&self.0.to_int(), start_index)
182 }
183}
184
185impl Column for BreakpointStateWrapper<'_> {
186 fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> {
187 let state = statement.column_int(start_index)?;
188
189 match state {
190 0 => Ok((BreakpointState::Enabled.into(), start_index + 1)),
191 1 => Ok((BreakpointState::Disabled.into(), start_index + 1)),
192 _ => anyhow::bail!("Invalid BreakpointState discriminant {state}"),
193 }
194 }
195}
196
197impl sqlez::bindable::StaticColumnCount for Breakpoint {
198 fn column_count() -> usize {
199 // Position, log message, condition message, and hit condition message
200 4 + BreakpointStateWrapper::column_count()
201 }
202}
203
204impl sqlez::bindable::Bind for Breakpoint {
205 fn bind(
206 &self,
207 statement: &sqlez::statement::Statement,
208 start_index: i32,
209 ) -> anyhow::Result<i32> {
210 let next_index = statement.bind(&self.position, start_index)?;
211 let next_index = statement.bind(&self.message, next_index)?;
212 let next_index = statement.bind(&self.condition, next_index)?;
213 let next_index = statement.bind(&self.hit_condition, next_index)?;
214 statement.bind(
215 &BreakpointStateWrapper(Cow::Borrowed(&self.state)),
216 next_index,
217 )
218 }
219}
220
221impl Column for Breakpoint {
222 fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
223 let position = statement
224 .column_int(start_index)
225 .with_context(|| format!("Failed to read BreakPoint at index {start_index}"))?
226 as u32;
227 let (message, next_index) = Option::<String>::column(statement, start_index + 1)?;
228 let (condition, next_index) = Option::<String>::column(statement, next_index)?;
229 let (hit_condition, next_index) = Option::<String>::column(statement, next_index)?;
230 let (state, next_index) = BreakpointStateWrapper::column(statement, next_index)?;
231
232 Ok((
233 Breakpoint {
234 position,
235 message: message.map(Arc::from),
236 condition: condition.map(Arc::from),
237 hit_condition: hit_condition.map(Arc::from),
238 state: state.0.into_owned(),
239 },
240 next_index,
241 ))
242 }
243}
244
245#[derive(Clone, Debug, PartialEq)]
246struct SerializedPixels(gpui::Pixels);
247impl sqlez::bindable::StaticColumnCount for SerializedPixels {}
248
249impl sqlez::bindable::Bind for SerializedPixels {
250 fn bind(
251 &self,
252 statement: &sqlez::statement::Statement,
253 start_index: i32,
254 ) -> anyhow::Result<i32> {
255 let this: i32 = self.0.0 as i32;
256 this.bind(statement, start_index)
257 }
258}
259
260pub struct WorkspaceDb(ThreadSafeConnection);
261
262impl Domain for WorkspaceDb {
263 const NAME: &str = stringify!(WorkspaceDb);
264
265 const MIGRATIONS: &[&str] = &[
266 sql!(
267 CREATE TABLE workspaces(
268 workspace_id INTEGER PRIMARY KEY,
269 workspace_location BLOB UNIQUE,
270 dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
271 dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
272 dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed.
273 left_sidebar_open INTEGER, // Boolean
274 timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
275 FOREIGN KEY(dock_pane) REFERENCES panes(pane_id)
276 ) STRICT;
277
278 CREATE TABLE pane_groups(
279 group_id INTEGER PRIMARY KEY,
280 workspace_id INTEGER NOT NULL,
281 parent_group_id INTEGER, // NULL indicates that this is a root node
282 position INTEGER, // NULL indicates that this is a root node
283 axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal'
284 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
285 ON DELETE CASCADE
286 ON UPDATE CASCADE,
287 FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
288 ) STRICT;
289
290 CREATE TABLE panes(
291 pane_id INTEGER PRIMARY KEY,
292 workspace_id INTEGER NOT NULL,
293 active INTEGER NOT NULL, // Boolean
294 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
295 ON DELETE CASCADE
296 ON UPDATE CASCADE
297 ) STRICT;
298
299 CREATE TABLE center_panes(
300 pane_id INTEGER PRIMARY KEY,
301 parent_group_id INTEGER, // NULL means that this is a root pane
302 position INTEGER, // NULL means that this is a root pane
303 FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
304 ON DELETE CASCADE,
305 FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
306 ) STRICT;
307
308 CREATE TABLE items(
309 item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique
310 workspace_id INTEGER NOT NULL,
311 pane_id INTEGER NOT NULL,
312 kind TEXT NOT NULL,
313 position INTEGER NOT NULL,
314 active INTEGER NOT NULL,
315 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
316 ON DELETE CASCADE
317 ON UPDATE CASCADE,
318 FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
319 ON DELETE CASCADE,
320 PRIMARY KEY(item_id, workspace_id)
321 ) STRICT;
322 ),
323 sql!(
324 ALTER TABLE workspaces ADD COLUMN window_state TEXT;
325 ALTER TABLE workspaces ADD COLUMN window_x REAL;
326 ALTER TABLE workspaces ADD COLUMN window_y REAL;
327 ALTER TABLE workspaces ADD COLUMN window_width REAL;
328 ALTER TABLE workspaces ADD COLUMN window_height REAL;
329 ALTER TABLE workspaces ADD COLUMN display BLOB;
330 ),
331 // Drop foreign key constraint from workspaces.dock_pane to panes table.
332 sql!(
333 CREATE TABLE workspaces_2(
334 workspace_id INTEGER PRIMARY KEY,
335 workspace_location BLOB UNIQUE,
336 dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
337 dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
338 dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed.
339 left_sidebar_open INTEGER, // Boolean
340 timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
341 window_state TEXT,
342 window_x REAL,
343 window_y REAL,
344 window_width REAL,
345 window_height REAL,
346 display BLOB
347 ) STRICT;
348 INSERT INTO workspaces_2 SELECT * FROM workspaces;
349 DROP TABLE workspaces;
350 ALTER TABLE workspaces_2 RENAME TO workspaces;
351 ),
352 // Add panels related information
353 sql!(
354 ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool
355 ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT;
356 ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool
357 ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT;
358 ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool
359 ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT;
360 ),
361 // Add panel zoom persistence
362 sql!(
363 ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool
364 ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool
365 ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool
366 ),
367 // Add pane group flex data
368 sql!(
369 ALTER TABLE pane_groups ADD COLUMN flexes TEXT;
370 ),
371 // Add fullscreen field to workspace
372 // Deprecated, `WindowBounds` holds the fullscreen state now.
373 // Preserving so users can downgrade Zed.
374 sql!(
375 ALTER TABLE workspaces ADD COLUMN fullscreen INTEGER; //bool
376 ),
377 // Add preview field to items
378 sql!(
379 ALTER TABLE items ADD COLUMN preview INTEGER; //bool
380 ),
381 // Add centered_layout field to workspace
382 sql!(
383 ALTER TABLE workspaces ADD COLUMN centered_layout INTEGER; //bool
384 ),
385 sql!(
386 CREATE TABLE remote_projects (
387 remote_project_id INTEGER NOT NULL UNIQUE,
388 path TEXT,
389 dev_server_name TEXT
390 );
391 ALTER TABLE workspaces ADD COLUMN remote_project_id INTEGER;
392 ALTER TABLE workspaces RENAME COLUMN workspace_location TO local_paths;
393 ),
394 sql!(
395 DROP TABLE remote_projects;
396 CREATE TABLE dev_server_projects (
397 id INTEGER NOT NULL UNIQUE,
398 path TEXT,
399 dev_server_name TEXT
400 );
401 ALTER TABLE workspaces DROP COLUMN remote_project_id;
402 ALTER TABLE workspaces ADD COLUMN dev_server_project_id INTEGER;
403 ),
404 sql!(
405 ALTER TABLE workspaces ADD COLUMN local_paths_order BLOB;
406 ),
407 sql!(
408 ALTER TABLE workspaces ADD COLUMN session_id TEXT DEFAULT NULL;
409 ),
410 sql!(
411 ALTER TABLE workspaces ADD COLUMN window_id INTEGER DEFAULT NULL;
412 ),
413 sql!(
414 ALTER TABLE panes ADD COLUMN pinned_count INTEGER DEFAULT 0;
415 ),
416 sql!(
417 CREATE TABLE ssh_projects (
418 id INTEGER PRIMARY KEY,
419 host TEXT NOT NULL,
420 port INTEGER,
421 path TEXT NOT NULL,
422 user TEXT
423 );
424 ALTER TABLE workspaces ADD COLUMN ssh_project_id INTEGER REFERENCES ssh_projects(id) ON DELETE CASCADE;
425 ),
426 sql!(
427 ALTER TABLE ssh_projects RENAME COLUMN path TO paths;
428 ),
429 sql!(
430 CREATE TABLE toolchains (
431 workspace_id INTEGER,
432 worktree_id INTEGER,
433 language_name TEXT NOT NULL,
434 name TEXT NOT NULL,
435 path TEXT NOT NULL,
436 PRIMARY KEY (workspace_id, worktree_id, language_name)
437 );
438 ),
439 sql!(
440 ALTER TABLE toolchains ADD COLUMN raw_json TEXT DEFAULT "{}";
441 ),
442 sql!(
443 CREATE TABLE breakpoints (
444 workspace_id INTEGER NOT NULL,
445 path TEXT NOT NULL,
446 breakpoint_location INTEGER NOT NULL,
447 kind INTEGER NOT NULL,
448 log_message TEXT,
449 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
450 ON DELETE CASCADE
451 ON UPDATE CASCADE
452 );
453 ),
454 sql!(
455 ALTER TABLE workspaces ADD COLUMN local_paths_array TEXT;
456 CREATE UNIQUE INDEX local_paths_array_uq ON workspaces(local_paths_array);
457 ALTER TABLE workspaces ADD COLUMN local_paths_order_array TEXT;
458 ),
459 sql!(
460 ALTER TABLE breakpoints ADD COLUMN state INTEGER DEFAULT(0) NOT NULL
461 ),
462 sql!(
463 ALTER TABLE breakpoints DROP COLUMN kind
464 ),
465 sql!(ALTER TABLE toolchains ADD COLUMN relative_worktree_path TEXT DEFAULT "" NOT NULL),
466 sql!(
467 ALTER TABLE breakpoints ADD COLUMN condition TEXT;
468 ALTER TABLE breakpoints ADD COLUMN hit_condition TEXT;
469 ),
470 sql!(CREATE TABLE toolchains2 (
471 workspace_id INTEGER,
472 worktree_id INTEGER,
473 language_name TEXT NOT NULL,
474 name TEXT NOT NULL,
475 path TEXT NOT NULL,
476 raw_json TEXT NOT NULL,
477 relative_worktree_path TEXT NOT NULL,
478 PRIMARY KEY (workspace_id, worktree_id, language_name, relative_worktree_path)) STRICT;
479 INSERT INTO toolchains2
480 SELECT * FROM toolchains;
481 DROP TABLE toolchains;
482 ALTER TABLE toolchains2 RENAME TO toolchains;
483 ),
484 sql!(
485 CREATE TABLE ssh_connections (
486 id INTEGER PRIMARY KEY,
487 host TEXT NOT NULL,
488 port INTEGER,
489 user TEXT
490 );
491
492 INSERT INTO ssh_connections (host, port, user)
493 SELECT DISTINCT host, port, user
494 FROM ssh_projects;
495
496 CREATE TABLE workspaces_2(
497 workspace_id INTEGER PRIMARY KEY,
498 paths TEXT,
499 paths_order TEXT,
500 ssh_connection_id INTEGER REFERENCES ssh_connections(id),
501 timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
502 window_state TEXT,
503 window_x REAL,
504 window_y REAL,
505 window_width REAL,
506 window_height REAL,
507 display BLOB,
508 left_dock_visible INTEGER,
509 left_dock_active_panel TEXT,
510 right_dock_visible INTEGER,
511 right_dock_active_panel TEXT,
512 bottom_dock_visible INTEGER,
513 bottom_dock_active_panel TEXT,
514 left_dock_zoom INTEGER,
515 right_dock_zoom INTEGER,
516 bottom_dock_zoom INTEGER,
517 fullscreen INTEGER,
518 centered_layout INTEGER,
519 session_id TEXT,
520 window_id INTEGER
521 ) STRICT;
522
523 INSERT
524 INTO workspaces_2
525 SELECT
526 workspaces.workspace_id,
527 CASE
528 WHEN ssh_projects.id IS NOT NULL THEN ssh_projects.paths
529 ELSE
530 CASE
531 WHEN workspaces.local_paths_array IS NULL OR workspaces.local_paths_array = "" THEN
532 NULL
533 ELSE
534 replace(workspaces.local_paths_array, ',', CHAR(10))
535 END
536 END as paths,
537
538 CASE
539 WHEN ssh_projects.id IS NOT NULL THEN ""
540 ELSE workspaces.local_paths_order_array
541 END as paths_order,
542
543 CASE
544 WHEN ssh_projects.id IS NOT NULL THEN (
545 SELECT ssh_connections.id
546 FROM ssh_connections
547 WHERE
548 ssh_connections.host IS ssh_projects.host AND
549 ssh_connections.port IS ssh_projects.port AND
550 ssh_connections.user IS ssh_projects.user
551 )
552 ELSE NULL
553 END as ssh_connection_id,
554
555 workspaces.timestamp,
556 workspaces.window_state,
557 workspaces.window_x,
558 workspaces.window_y,
559 workspaces.window_width,
560 workspaces.window_height,
561 workspaces.display,
562 workspaces.left_dock_visible,
563 workspaces.left_dock_active_panel,
564 workspaces.right_dock_visible,
565 workspaces.right_dock_active_panel,
566 workspaces.bottom_dock_visible,
567 workspaces.bottom_dock_active_panel,
568 workspaces.left_dock_zoom,
569 workspaces.right_dock_zoom,
570 workspaces.bottom_dock_zoom,
571 workspaces.fullscreen,
572 workspaces.centered_layout,
573 workspaces.session_id,
574 workspaces.window_id
575 FROM
576 workspaces LEFT JOIN
577 ssh_projects ON
578 workspaces.ssh_project_id = ssh_projects.id;
579
580 DELETE FROM workspaces_2
581 WHERE workspace_id NOT IN (
582 SELECT MAX(workspace_id)
583 FROM workspaces_2
584 GROUP BY ssh_connection_id, paths
585 );
586
587 DROP TABLE ssh_projects;
588 DROP TABLE workspaces;
589 ALTER TABLE workspaces_2 RENAME TO workspaces;
590
591 CREATE UNIQUE INDEX ix_workspaces_location ON workspaces(ssh_connection_id, paths);
592 ),
593 // Fix any data from when workspaces.paths were briefly encoded as JSON arrays
594 sql!(
595 UPDATE workspaces
596 SET paths = CASE
597 WHEN substr(paths, 1, 2) = '[' || '"' AND substr(paths, -2, 2) = '"' || ']' THEN
598 replace(
599 substr(paths, 3, length(paths) - 4),
600 '"' || ',' || '"',
601 CHAR(10)
602 )
603 ELSE
604 replace(paths, ',', CHAR(10))
605 END
606 WHERE paths IS NOT NULL
607 ),
608 sql!(
609 CREATE TABLE remote_connections(
610 id INTEGER PRIMARY KEY,
611 kind TEXT NOT NULL,
612 host TEXT,
613 port INTEGER,
614 user TEXT,
615 distro TEXT
616 );
617
618 CREATE TABLE workspaces_2(
619 workspace_id INTEGER PRIMARY KEY,
620 paths TEXT,
621 paths_order TEXT,
622 remote_connection_id INTEGER REFERENCES remote_connections(id),
623 timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
624 window_state TEXT,
625 window_x REAL,
626 window_y REAL,
627 window_width REAL,
628 window_height REAL,
629 display BLOB,
630 left_dock_visible INTEGER,
631 left_dock_active_panel TEXT,
632 right_dock_visible INTEGER,
633 right_dock_active_panel TEXT,
634 bottom_dock_visible INTEGER,
635 bottom_dock_active_panel TEXT,
636 left_dock_zoom INTEGER,
637 right_dock_zoom INTEGER,
638 bottom_dock_zoom INTEGER,
639 fullscreen INTEGER,
640 centered_layout INTEGER,
641 session_id TEXT,
642 window_id INTEGER
643 ) STRICT;
644
645 INSERT INTO remote_connections
646 SELECT
647 id,
648 "ssh" as kind,
649 host,
650 port,
651 user,
652 NULL as distro
653 FROM ssh_connections;
654
655 INSERT
656 INTO workspaces_2
657 SELECT
658 workspace_id,
659 paths,
660 paths_order,
661 ssh_connection_id as remote_connection_id,
662 timestamp,
663 window_state,
664 window_x,
665 window_y,
666 window_width,
667 window_height,
668 display,
669 left_dock_visible,
670 left_dock_active_panel,
671 right_dock_visible,
672 right_dock_active_panel,
673 bottom_dock_visible,
674 bottom_dock_active_panel,
675 left_dock_zoom,
676 right_dock_zoom,
677 bottom_dock_zoom,
678 fullscreen,
679 centered_layout,
680 session_id,
681 window_id
682 FROM
683 workspaces;
684
685 DROP TABLE workspaces;
686 ALTER TABLE workspaces_2 RENAME TO workspaces;
687
688 CREATE UNIQUE INDEX ix_workspaces_location ON workspaces(remote_connection_id, paths);
689 ),
690 sql!(CREATE TABLE user_toolchains (
691 remote_connection_id INTEGER,
692 workspace_id INTEGER NOT NULL,
693 worktree_id INTEGER NOT NULL,
694 relative_worktree_path TEXT NOT NULL,
695 language_name TEXT NOT NULL,
696 name TEXT NOT NULL,
697 path TEXT NOT NULL,
698 raw_json TEXT NOT NULL,
699
700 PRIMARY KEY (workspace_id, worktree_id, relative_worktree_path, language_name, name, path, raw_json)
701 ) STRICT;),
702 sql!(
703 DROP TABLE ssh_connections;
704 ),
705 ];
706
707 // Allow recovering from bad migration that was initially shipped to nightly
708 // when introducing the ssh_connections table.
709 fn should_allow_migration_change(_index: usize, old: &str, new: &str) -> bool {
710 old.starts_with("CREATE TABLE ssh_connections")
711 && new.starts_with("CREATE TABLE ssh_connections")
712 }
713}
714
715db::static_connection!(DB, WorkspaceDb, []);
716
717impl WorkspaceDb {
718 /// Returns a serialized workspace for the given worktree_roots. If the passed array
719 /// is empty, the most recent workspace is returned instead. If no workspace for the
720 /// passed roots is stored, returns none.
721 pub(crate) fn workspace_for_roots<P: AsRef<Path>>(
722 &self,
723 worktree_roots: &[P],
724 ) -> Option<SerializedWorkspace> {
725 self.workspace_for_roots_internal(worktree_roots, None)
726 }
727
728 pub(crate) fn remote_workspace_for_roots<P: AsRef<Path>>(
729 &self,
730 worktree_roots: &[P],
731 ssh_project_id: RemoteConnectionId,
732 ) -> Option<SerializedWorkspace> {
733 self.workspace_for_roots_internal(worktree_roots, Some(ssh_project_id))
734 }
735
736 pub(crate) fn workspace_for_roots_internal<P: AsRef<Path>>(
737 &self,
738 worktree_roots: &[P],
739 remote_connection_id: Option<RemoteConnectionId>,
740 ) -> Option<SerializedWorkspace> {
741 // paths are sorted before db interactions to ensure that the order of the paths
742 // doesn't affect the workspace selection for existing workspaces
743 let root_paths = PathList::new(worktree_roots);
744
745 // Note that we re-assign the workspace_id here in case it's empty
746 // and we've grabbed the most recent workspace
747 let (
748 workspace_id,
749 paths,
750 paths_order,
751 window_bounds,
752 display,
753 centered_layout,
754 docks,
755 window_id,
756 ): (
757 WorkspaceId,
758 String,
759 String,
760 Option<SerializedWindowBounds>,
761 Option<Uuid>,
762 Option<bool>,
763 DockStructure,
764 Option<u64>,
765 ) = self
766 .select_row_bound(sql! {
767 SELECT
768 workspace_id,
769 paths,
770 paths_order,
771 window_state,
772 window_x,
773 window_y,
774 window_width,
775 window_height,
776 display,
777 centered_layout,
778 left_dock_visible,
779 left_dock_active_panel,
780 left_dock_zoom,
781 right_dock_visible,
782 right_dock_active_panel,
783 right_dock_zoom,
784 bottom_dock_visible,
785 bottom_dock_active_panel,
786 bottom_dock_zoom,
787 window_id
788 FROM workspaces
789 WHERE
790 paths IS ? AND
791 remote_connection_id IS ?
792 LIMIT 1
793 })
794 .map(|mut prepared_statement| {
795 (prepared_statement)((
796 root_paths.serialize().paths,
797 remote_connection_id.map(|id| id.0 as i32),
798 ))
799 .unwrap()
800 })
801 .context("No workspaces found")
802 .warn_on_err()
803 .flatten()?;
804
805 let paths = PathList::deserialize(&SerializedPathList {
806 paths,
807 order: paths_order,
808 });
809
810 Some(SerializedWorkspace {
811 id: workspace_id,
812 location: SerializedWorkspaceLocation::Local,
813 paths,
814 center_group: self
815 .get_center_pane_group(workspace_id)
816 .context("Getting center group")
817 .log_err()?,
818 window_bounds,
819 centered_layout: centered_layout.unwrap_or(false),
820 display,
821 docks,
822 session_id: None,
823 breakpoints: self.breakpoints(workspace_id),
824 window_id,
825 user_toolchains: self.user_toolchains(workspace_id, remote_connection_id),
826 })
827 }
828
829 fn breakpoints(&self, workspace_id: WorkspaceId) -> BTreeMap<Arc<Path>, Vec<SourceBreakpoint>> {
830 let breakpoints: Result<Vec<(PathBuf, Breakpoint)>> = self
831 .select_bound(sql! {
832 SELECT path, breakpoint_location, log_message, condition, hit_condition, state
833 FROM breakpoints
834 WHERE workspace_id = ?
835 })
836 .and_then(|mut prepared_statement| (prepared_statement)(workspace_id));
837
838 match breakpoints {
839 Ok(bp) => {
840 if bp.is_empty() {
841 log::debug!("Breakpoints are empty after querying database for them");
842 }
843
844 let mut map: BTreeMap<Arc<Path>, Vec<SourceBreakpoint>> = Default::default();
845
846 for (path, breakpoint) in bp {
847 let path: Arc<Path> = path.into();
848 map.entry(path.clone()).or_default().push(SourceBreakpoint {
849 row: breakpoint.position,
850 path,
851 message: breakpoint.message,
852 condition: breakpoint.condition,
853 hit_condition: breakpoint.hit_condition,
854 state: breakpoint.state,
855 });
856 }
857
858 for (path, bps) in map.iter() {
859 log::info!(
860 "Got {} breakpoints from database at path: {}",
861 bps.len(),
862 path.to_string_lossy()
863 );
864 }
865
866 map
867 }
868 Err(msg) => {
869 log::error!("Breakpoints query failed with msg: {msg}");
870 Default::default()
871 }
872 }
873 }
874
875 fn user_toolchains(
876 &self,
877 workspace_id: WorkspaceId,
878 remote_connection_id: Option<RemoteConnectionId>,
879 ) -> BTreeMap<ToolchainScope, IndexSet<Toolchain>> {
880 type RowKind = (WorkspaceId, u64, String, String, String, String, String);
881
882 let toolchains: Vec<RowKind> = self
883 .select_bound(sql! {
884 SELECT workspace_id, worktree_id, relative_worktree_path,
885 language_name, name, path, raw_json
886 FROM user_toolchains WHERE remote_connection_id IS ?1 AND (
887 workspace_id IN (0, ?2)
888 )
889 })
890 .and_then(|mut statement| {
891 (statement)((remote_connection_id.map(|id| id.0), workspace_id))
892 })
893 .unwrap_or_default();
894 let mut ret = BTreeMap::<_, IndexSet<_>>::default();
895
896 for (
897 _workspace_id,
898 worktree_id,
899 relative_worktree_path,
900 language_name,
901 name,
902 path,
903 raw_json,
904 ) in toolchains
905 {
906 // INTEGER's that are primary keys (like workspace ids, remote connection ids and such) start at 1, so we're safe to
907 let scope = if _workspace_id == WorkspaceId(0) {
908 debug_assert_eq!(worktree_id, u64::MAX);
909 debug_assert_eq!(relative_worktree_path, String::default());
910 ToolchainScope::Global
911 } else {
912 debug_assert_eq!(workspace_id, _workspace_id);
913 debug_assert_eq!(
914 worktree_id == u64::MAX,
915 relative_worktree_path == String::default()
916 );
917
918 if worktree_id != u64::MAX && relative_worktree_path != String::default() {
919 ToolchainScope::Subproject(
920 WorktreeId::from_usize(worktree_id as usize),
921 Arc::from(relative_worktree_path.as_ref()),
922 )
923 } else {
924 ToolchainScope::Project
925 }
926 };
927 let Ok(as_json) = serde_json::from_str(&raw_json) else {
928 continue;
929 };
930 let toolchain = Toolchain {
931 name: SharedString::from(name),
932 path: SharedString::from(path),
933 language_name: LanguageName::from_proto(language_name),
934 as_json,
935 };
936 ret.entry(scope).or_default().insert(toolchain);
937 }
938
939 ret
940 }
941
942 /// Saves a workspace using the worktree roots. Will garbage collect any workspaces
943 /// that used this workspace previously
944 pub(crate) async fn save_workspace(&self, workspace: SerializedWorkspace) {
945 let paths = workspace.paths.serialize();
946 log::debug!("Saving workspace at location: {:?}", workspace.location);
947 self.write(move |conn| {
948 conn.with_savepoint("update_worktrees", || {
949 let remote_connection_id = match workspace.location.clone() {
950 SerializedWorkspaceLocation::Local => None,
951 SerializedWorkspaceLocation::Remote(connection_options) => {
952 Some(Self::get_or_create_remote_connection_internal(
953 conn,
954 connection_options
955 )?.0)
956 }
957 };
958
959 // Clear out panes and pane_groups
960 conn.exec_bound(sql!(
961 DELETE FROM pane_groups WHERE workspace_id = ?1;
962 DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)
963 .context("Clearing old panes")?;
964
965 conn.exec_bound(
966 sql!(
967 DELETE FROM breakpoints WHERE workspace_id = ?1;
968 )
969 )?(workspace.id).context("Clearing old breakpoints")?;
970
971 for (path, breakpoints) in workspace.breakpoints {
972 for bp in breakpoints {
973 let state = BreakpointStateWrapper::from(bp.state);
974 match conn.exec_bound(sql!(
975 INSERT INTO breakpoints (workspace_id, path, breakpoint_location, log_message, condition, hit_condition, state)
976 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7);))?
977
978 ((
979 workspace.id,
980 path.as_ref(),
981 bp.row,
982 bp.message,
983 bp.condition,
984 bp.hit_condition,
985 state,
986 )) {
987 Ok(_) => {
988 log::debug!("Stored breakpoint at row: {} in path: {}", bp.row, path.to_string_lossy())
989 }
990 Err(err) => {
991 log::error!("{err}");
992 continue;
993 }
994 }
995 }
996 }
997 for (scope, toolchains) in workspace.user_toolchains {
998 for toolchain in toolchains {
999 let query = sql!(INSERT OR REPLACE INTO user_toolchains(remote_connection_id, workspace_id, worktree_id, relative_worktree_path, language_name, name, path, raw_json) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8));
1000 let (workspace_id, worktree_id, relative_worktree_path) = match scope {
1001 ToolchainScope::Subproject(worktree_id, ref path) => (Some(workspace.id), Some(worktree_id), Some(path.to_string_lossy().into_owned())),
1002 ToolchainScope::Project => (Some(workspace.id), None, None),
1003 ToolchainScope::Global => (None, None, None),
1004 };
1005 let args = (remote_connection_id, workspace_id.unwrap_or(WorkspaceId(0)), worktree_id.map_or(usize::MAX,|id| id.to_usize()), relative_worktree_path.unwrap_or_default(),
1006 toolchain.language_name.as_ref().to_owned(), toolchain.name.to_string(), toolchain.path.to_string(), toolchain.as_json.to_string());
1007 if let Err(err) = conn.exec_bound(query)?(args) {
1008 log::error!("{err}");
1009 continue;
1010 }
1011 }
1012 }
1013
1014 conn.exec_bound(sql!(
1015 DELETE
1016 FROM workspaces
1017 WHERE
1018 workspace_id != ?1 AND
1019 paths IS ?2 AND
1020 remote_connection_id IS ?3
1021 ))?((
1022 workspace.id,
1023 paths.paths.clone(),
1024 remote_connection_id,
1025 ))
1026 .context("clearing out old locations")?;
1027
1028 // Upsert
1029 let query = sql!(
1030 INSERT INTO workspaces(
1031 workspace_id,
1032 paths,
1033 paths_order,
1034 remote_connection_id,
1035 left_dock_visible,
1036 left_dock_active_panel,
1037 left_dock_zoom,
1038 right_dock_visible,
1039 right_dock_active_panel,
1040 right_dock_zoom,
1041 bottom_dock_visible,
1042 bottom_dock_active_panel,
1043 bottom_dock_zoom,
1044 session_id,
1045 window_id,
1046 timestamp
1047 )
1048 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, CURRENT_TIMESTAMP)
1049 ON CONFLICT DO
1050 UPDATE SET
1051 paths = ?2,
1052 paths_order = ?3,
1053 remote_connection_id = ?4,
1054 left_dock_visible = ?5,
1055 left_dock_active_panel = ?6,
1056 left_dock_zoom = ?7,
1057 right_dock_visible = ?8,
1058 right_dock_active_panel = ?9,
1059 right_dock_zoom = ?10,
1060 bottom_dock_visible = ?11,
1061 bottom_dock_active_panel = ?12,
1062 bottom_dock_zoom = ?13,
1063 session_id = ?14,
1064 window_id = ?15,
1065 timestamp = CURRENT_TIMESTAMP
1066 );
1067 let mut prepared_query = conn.exec_bound(query)?;
1068 let args = (
1069 workspace.id,
1070 paths.paths.clone(),
1071 paths.order.clone(),
1072 remote_connection_id,
1073 workspace.docks,
1074 workspace.session_id,
1075 workspace.window_id,
1076 );
1077
1078 prepared_query(args).context("Updating workspace")?;
1079
1080 // Save center pane group
1081 Self::save_pane_group(conn, workspace.id, &workspace.center_group, None)
1082 .context("save pane group in save workspace")?;
1083
1084 Ok(())
1085 })
1086 .log_err();
1087 })
1088 .await;
1089 }
1090
1091 pub(crate) async fn get_or_create_remote_connection(
1092 &self,
1093 options: RemoteConnectionOptions,
1094 ) -> Result<RemoteConnectionId> {
1095 self.write(move |conn| Self::get_or_create_remote_connection_internal(conn, options))
1096 .await
1097 }
1098
1099 fn get_or_create_remote_connection_internal(
1100 this: &Connection,
1101 options: RemoteConnectionOptions,
1102 ) -> Result<RemoteConnectionId> {
1103 let kind;
1104 let user;
1105 let mut host = None;
1106 let mut port = None;
1107 let mut distro = None;
1108 match options {
1109 RemoteConnectionOptions::Ssh(options) => {
1110 kind = RemoteConnectionKind::Ssh;
1111 host = Some(options.host);
1112 port = options.port;
1113 user = options.username;
1114 }
1115 RemoteConnectionOptions::Wsl(options) => {
1116 kind = RemoteConnectionKind::Wsl;
1117 distro = Some(options.distro_name);
1118 user = options.user;
1119 }
1120 }
1121 Self::get_or_create_remote_connection_query(this, kind, host, port, user, distro)
1122 }
1123
1124 fn get_or_create_remote_connection_query(
1125 this: &Connection,
1126 kind: RemoteConnectionKind,
1127 host: Option<String>,
1128 port: Option<u16>,
1129 user: Option<String>,
1130 distro: Option<String>,
1131 ) -> Result<RemoteConnectionId> {
1132 if let Some(id) = this.select_row_bound(sql!(
1133 SELECT id
1134 FROM remote_connections
1135 WHERE
1136 kind IS ? AND
1137 host IS ? AND
1138 port IS ? AND
1139 user IS ? AND
1140 distro IS ?
1141 LIMIT 1
1142 ))?((
1143 kind.serialize(),
1144 host.clone(),
1145 port,
1146 user.clone(),
1147 distro.clone(),
1148 ))? {
1149 Ok(RemoteConnectionId(id))
1150 } else {
1151 let id = this.select_row_bound(sql!(
1152 INSERT INTO remote_connections (
1153 kind,
1154 host,
1155 port,
1156 user,
1157 distro
1158 ) VALUES (?1, ?2, ?3, ?4, ?5)
1159 RETURNING id
1160 ))?((kind.serialize(), host, port, user, distro))?
1161 .context("failed to insert remote project")?;
1162 Ok(RemoteConnectionId(id))
1163 }
1164 }
1165
1166 query! {
1167 pub async fn next_id() -> Result<WorkspaceId> {
1168 INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id
1169 }
1170 }
1171
1172 fn recent_workspaces(
1173 &self,
1174 ) -> Result<Vec<(WorkspaceId, PathList, Option<RemoteConnectionId>)>> {
1175 Ok(self
1176 .recent_workspaces_query()?
1177 .into_iter()
1178 .map(|(id, paths, order, remote_connection_id)| {
1179 (
1180 id,
1181 PathList::deserialize(&SerializedPathList { paths, order }),
1182 remote_connection_id.map(RemoteConnectionId),
1183 )
1184 })
1185 .collect())
1186 }
1187
1188 query! {
1189 fn recent_workspaces_query() -> Result<Vec<(WorkspaceId, String, String, Option<u64>)>> {
1190 SELECT workspace_id, paths, paths_order, remote_connection_id
1191 FROM workspaces
1192 WHERE
1193 paths IS NOT NULL OR
1194 remote_connection_id IS NOT NULL
1195 ORDER BY timestamp DESC
1196 }
1197 }
1198
1199 fn session_workspaces(
1200 &self,
1201 session_id: String,
1202 ) -> Result<Vec<(PathList, Option<u64>, Option<RemoteConnectionId>)>> {
1203 Ok(self
1204 .session_workspaces_query(session_id)?
1205 .into_iter()
1206 .map(|(paths, order, window_id, remote_connection_id)| {
1207 (
1208 PathList::deserialize(&SerializedPathList { paths, order }),
1209 window_id,
1210 remote_connection_id.map(RemoteConnectionId),
1211 )
1212 })
1213 .collect())
1214 }
1215
1216 query! {
1217 fn session_workspaces_query(session_id: String) -> Result<Vec<(String, String, Option<u64>, Option<u64>)>> {
1218 SELECT paths, paths_order, window_id, remote_connection_id
1219 FROM workspaces
1220 WHERE session_id = ?1
1221 ORDER BY timestamp DESC
1222 }
1223 }
1224
1225 query! {
1226 pub fn breakpoints_for_file(workspace_id: WorkspaceId, file_path: &Path) -> Result<Vec<Breakpoint>> {
1227 SELECT breakpoint_location
1228 FROM breakpoints
1229 WHERE workspace_id= ?1 AND path = ?2
1230 }
1231 }
1232
1233 query! {
1234 pub fn clear_breakpoints(file_path: &Path) -> Result<()> {
1235 DELETE FROM breakpoints
1236 WHERE file_path = ?2
1237 }
1238 }
1239
1240 fn remote_connections(&self) -> Result<HashMap<RemoteConnectionId, RemoteConnectionOptions>> {
1241 Ok(self.select(sql!(
1242 SELECT
1243 id, kind, host, port, user, distro
1244 FROM
1245 remote_connections
1246 ))?()?
1247 .into_iter()
1248 .filter_map(|(id, kind, host, port, user, distro)| {
1249 Some((
1250 RemoteConnectionId(id),
1251 Self::remote_connection_from_row(kind, host, port, user, distro)?,
1252 ))
1253 })
1254 .collect())
1255 }
1256
1257 pub(crate) fn remote_connection(
1258 &self,
1259 id: RemoteConnectionId,
1260 ) -> Result<RemoteConnectionOptions> {
1261 let (kind, host, port, user, distro) = self.select_row_bound(sql!(
1262 SELECT kind, host, port, user, distro
1263 FROM remote_connections
1264 WHERE id = ?
1265 ))?(id.0)?
1266 .context("no such remote connection")?;
1267 Self::remote_connection_from_row(kind, host, port, user, distro)
1268 .context("invalid remote_connection row")
1269 }
1270
1271 fn remote_connection_from_row(
1272 kind: String,
1273 host: Option<String>,
1274 port: Option<u16>,
1275 user: Option<String>,
1276 distro: Option<String>,
1277 ) -> Option<RemoteConnectionOptions> {
1278 match RemoteConnectionKind::deserialize(&kind)? {
1279 RemoteConnectionKind::Wsl => Some(RemoteConnectionOptions::Wsl(WslConnectionOptions {
1280 distro_name: distro?,
1281 user: user,
1282 })),
1283 RemoteConnectionKind::Ssh => Some(RemoteConnectionOptions::Ssh(SshConnectionOptions {
1284 host: host?,
1285 port,
1286 username: user,
1287 ..Default::default()
1288 })),
1289 }
1290 }
1291
1292 pub(crate) fn last_window(
1293 &self,
1294 ) -> anyhow::Result<(Option<Uuid>, Option<SerializedWindowBounds>)> {
1295 let mut prepared_query =
1296 self.select::<(Option<Uuid>, Option<SerializedWindowBounds>)>(sql!(
1297 SELECT
1298 display,
1299 window_state, window_x, window_y, window_width, window_height
1300 FROM workspaces
1301 WHERE paths
1302 IS NOT NULL
1303 ORDER BY timestamp DESC
1304 LIMIT 1
1305 ))?;
1306 let result = prepared_query()?;
1307 Ok(result.into_iter().next().unwrap_or((None, None)))
1308 }
1309
1310 query! {
1311 pub async fn delete_workspace_by_id(id: WorkspaceId) -> Result<()> {
1312 DELETE FROM workspaces
1313 WHERE workspace_id IS ?
1314 }
1315 }
1316
1317 // Returns the recent locations which are still valid on disk and deletes ones which no longer
1318 // exist.
1319 pub async fn recent_workspaces_on_disk(
1320 &self,
1321 ) -> Result<Vec<(WorkspaceId, SerializedWorkspaceLocation, PathList)>> {
1322 let mut result = Vec::new();
1323 let mut delete_tasks = Vec::new();
1324 let remote_connections = self.remote_connections()?;
1325
1326 for (id, paths, remote_connection_id) in self.recent_workspaces()? {
1327 if let Some(remote_connection_id) = remote_connection_id {
1328 if let Some(connection_options) = remote_connections.get(&remote_connection_id) {
1329 result.push((
1330 id,
1331 SerializedWorkspaceLocation::Remote(connection_options.clone()),
1332 paths,
1333 ));
1334 } else {
1335 delete_tasks.push(self.delete_workspace_by_id(id));
1336 }
1337 continue;
1338 }
1339
1340 if paths.paths().iter().all(|path| path.exists())
1341 && paths.paths().iter().any(|path| path.is_dir())
1342 {
1343 result.push((id, SerializedWorkspaceLocation::Local, paths));
1344 } else {
1345 delete_tasks.push(self.delete_workspace_by_id(id));
1346 }
1347 }
1348
1349 futures::future::join_all(delete_tasks).await;
1350 Ok(result)
1351 }
1352
1353 pub async fn last_workspace(&self) -> Result<Option<(SerializedWorkspaceLocation, PathList)>> {
1354 Ok(self
1355 .recent_workspaces_on_disk()
1356 .await?
1357 .into_iter()
1358 .next()
1359 .map(|(_, location, paths)| (location, paths)))
1360 }
1361
1362 // Returns the locations of the workspaces that were still opened when the last
1363 // session was closed (i.e. when Zed was quit).
1364 // If `last_session_window_order` is provided, the returned locations are ordered
1365 // according to that.
1366 pub fn last_session_workspace_locations(
1367 &self,
1368 last_session_id: &str,
1369 last_session_window_stack: Option<Vec<WindowId>>,
1370 ) -> Result<Vec<(SerializedWorkspaceLocation, PathList)>> {
1371 let mut workspaces = Vec::new();
1372
1373 for (paths, window_id, remote_connection_id) in
1374 self.session_workspaces(last_session_id.to_owned())?
1375 {
1376 if let Some(remote_connection_id) = remote_connection_id {
1377 workspaces.push((
1378 SerializedWorkspaceLocation::Remote(
1379 self.remote_connection(remote_connection_id)?,
1380 ),
1381 paths,
1382 window_id.map(WindowId::from),
1383 ));
1384 } else if paths.paths().iter().all(|path| path.exists())
1385 && paths.paths().iter().any(|path| path.is_dir())
1386 {
1387 workspaces.push((
1388 SerializedWorkspaceLocation::Local,
1389 paths,
1390 window_id.map(WindowId::from),
1391 ));
1392 }
1393 }
1394
1395 if let Some(stack) = last_session_window_stack {
1396 workspaces.sort_by_key(|(_, _, window_id)| {
1397 window_id
1398 .and_then(|id| stack.iter().position(|&order_id| order_id == id))
1399 .unwrap_or(usize::MAX)
1400 });
1401 }
1402
1403 Ok(workspaces
1404 .into_iter()
1405 .map(|(location, paths, _)| (location, paths))
1406 .collect::<Vec<_>>())
1407 }
1408
1409 fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
1410 Ok(self
1411 .get_pane_group(workspace_id, None)?
1412 .into_iter()
1413 .next()
1414 .unwrap_or_else(|| {
1415 SerializedPaneGroup::Pane(SerializedPane {
1416 active: true,
1417 children: vec![],
1418 pinned_count: 0,
1419 })
1420 }))
1421 }
1422
1423 fn get_pane_group(
1424 &self,
1425 workspace_id: WorkspaceId,
1426 group_id: Option<GroupId>,
1427 ) -> Result<Vec<SerializedPaneGroup>> {
1428 type GroupKey = (Option<GroupId>, WorkspaceId);
1429 type GroupOrPane = (
1430 Option<GroupId>,
1431 Option<SerializedAxis>,
1432 Option<PaneId>,
1433 Option<bool>,
1434 Option<usize>,
1435 Option<String>,
1436 );
1437 self.select_bound::<GroupKey, GroupOrPane>(sql!(
1438 SELECT group_id, axis, pane_id, active, pinned_count, flexes
1439 FROM (SELECT
1440 group_id,
1441 axis,
1442 NULL as pane_id,
1443 NULL as active,
1444 NULL as pinned_count,
1445 position,
1446 parent_group_id,
1447 workspace_id,
1448 flexes
1449 FROM pane_groups
1450 UNION
1451 SELECT
1452 NULL,
1453 NULL,
1454 center_panes.pane_id,
1455 panes.active as active,
1456 pinned_count,
1457 position,
1458 parent_group_id,
1459 panes.workspace_id as workspace_id,
1460 NULL
1461 FROM center_panes
1462 JOIN panes ON center_panes.pane_id = panes.pane_id)
1463 WHERE parent_group_id IS ? AND workspace_id = ?
1464 ORDER BY position
1465 ))?((group_id, workspace_id))?
1466 .into_iter()
1467 .map(|(group_id, axis, pane_id, active, pinned_count, flexes)| {
1468 let maybe_pane = maybe!({ Some((pane_id?, active?, pinned_count?)) });
1469 if let Some((group_id, axis)) = group_id.zip(axis) {
1470 let flexes = flexes
1471 .map(|flexes: String| serde_json::from_str::<Vec<f32>>(&flexes))
1472 .transpose()?;
1473
1474 Ok(SerializedPaneGroup::Group {
1475 axis,
1476 children: self.get_pane_group(workspace_id, Some(group_id))?,
1477 flexes,
1478 })
1479 } else if let Some((pane_id, active, pinned_count)) = maybe_pane {
1480 Ok(SerializedPaneGroup::Pane(SerializedPane::new(
1481 self.get_items(pane_id)?,
1482 active,
1483 pinned_count,
1484 )))
1485 } else {
1486 bail!("Pane Group Child was neither a pane group or a pane");
1487 }
1488 })
1489 // Filter out panes and pane groups which don't have any children or items
1490 .filter(|pane_group| match pane_group {
1491 Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
1492 Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
1493 _ => true,
1494 })
1495 .collect::<Result<_>>()
1496 }
1497
1498 fn save_pane_group(
1499 conn: &Connection,
1500 workspace_id: WorkspaceId,
1501 pane_group: &SerializedPaneGroup,
1502 parent: Option<(GroupId, usize)>,
1503 ) -> Result<()> {
1504 if parent.is_none() {
1505 log::debug!("Saving a pane group for workspace {workspace_id:?}");
1506 }
1507 match pane_group {
1508 SerializedPaneGroup::Group {
1509 axis,
1510 children,
1511 flexes,
1512 } => {
1513 let (parent_id, position) = parent.unzip();
1514
1515 let flex_string = flexes
1516 .as_ref()
1517 .map(|flexes| serde_json::json!(flexes).to_string());
1518
1519 let group_id = conn.select_row_bound::<_, i64>(sql!(
1520 INSERT INTO pane_groups(
1521 workspace_id,
1522 parent_group_id,
1523 position,
1524 axis,
1525 flexes
1526 )
1527 VALUES (?, ?, ?, ?, ?)
1528 RETURNING group_id
1529 ))?((
1530 workspace_id,
1531 parent_id,
1532 position,
1533 *axis,
1534 flex_string,
1535 ))?
1536 .context("Couldn't retrieve group_id from inserted pane_group")?;
1537
1538 for (position, group) in children.iter().enumerate() {
1539 Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))?
1540 }
1541
1542 Ok(())
1543 }
1544 SerializedPaneGroup::Pane(pane) => {
1545 Self::save_pane(conn, workspace_id, pane, parent)?;
1546 Ok(())
1547 }
1548 }
1549 }
1550
1551 fn save_pane(
1552 conn: &Connection,
1553 workspace_id: WorkspaceId,
1554 pane: &SerializedPane,
1555 parent: Option<(GroupId, usize)>,
1556 ) -> Result<PaneId> {
1557 let pane_id = conn.select_row_bound::<_, i64>(sql!(
1558 INSERT INTO panes(workspace_id, active, pinned_count)
1559 VALUES (?, ?, ?)
1560 RETURNING pane_id
1561 ))?((workspace_id, pane.active, pane.pinned_count))?
1562 .context("Could not retrieve inserted pane_id")?;
1563
1564 let (parent_id, order) = parent.unzip();
1565 conn.exec_bound(sql!(
1566 INSERT INTO center_panes(pane_id, parent_group_id, position)
1567 VALUES (?, ?, ?)
1568 ))?((pane_id, parent_id, order))?;
1569
1570 Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
1571
1572 Ok(pane_id)
1573 }
1574
1575 fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
1576 self.select_bound(sql!(
1577 SELECT kind, item_id, active, preview FROM items
1578 WHERE pane_id = ?
1579 ORDER BY position
1580 ))?(pane_id)
1581 }
1582
1583 fn save_items(
1584 conn: &Connection,
1585 workspace_id: WorkspaceId,
1586 pane_id: PaneId,
1587 items: &[SerializedItem],
1588 ) -> Result<()> {
1589 let mut insert = conn.exec_bound(sql!(
1590 INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active, preview) VALUES (?, ?, ?, ?, ?, ?, ?)
1591 )).context("Preparing insertion")?;
1592 for (position, item) in items.iter().enumerate() {
1593 insert((workspace_id, pane_id, position, item))?;
1594 }
1595
1596 Ok(())
1597 }
1598
1599 query! {
1600 pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> {
1601 UPDATE workspaces
1602 SET timestamp = CURRENT_TIMESTAMP
1603 WHERE workspace_id = ?
1604 }
1605 }
1606
1607 query! {
1608 pub(crate) async fn set_window_open_status(workspace_id: WorkspaceId, bounds: SerializedWindowBounds, display: Uuid) -> Result<()> {
1609 UPDATE workspaces
1610 SET window_state = ?2,
1611 window_x = ?3,
1612 window_y = ?4,
1613 window_width = ?5,
1614 window_height = ?6,
1615 display = ?7
1616 WHERE workspace_id = ?1
1617 }
1618 }
1619
1620 query! {
1621 pub(crate) async fn set_centered_layout(workspace_id: WorkspaceId, centered_layout: bool) -> Result<()> {
1622 UPDATE workspaces
1623 SET centered_layout = ?2
1624 WHERE workspace_id = ?1
1625 }
1626 }
1627
1628 query! {
1629 pub(crate) async fn set_session_id(workspace_id: WorkspaceId, session_id: Option<String>) -> Result<()> {
1630 UPDATE workspaces
1631 SET session_id = ?2
1632 WHERE workspace_id = ?1
1633 }
1634 }
1635
1636 pub async fn toolchain(
1637 &self,
1638 workspace_id: WorkspaceId,
1639 worktree_id: WorktreeId,
1640 relative_worktree_path: String,
1641 language_name: LanguageName,
1642 ) -> Result<Option<Toolchain>> {
1643 self.write(move |this| {
1644 let mut select = this
1645 .select_bound(sql!(
1646 SELECT name, path, raw_json FROM toolchains WHERE workspace_id = ? AND language_name = ? AND worktree_id = ? AND relative_worktree_path = ?
1647 ))
1648 .context("select toolchain")?;
1649
1650 let toolchain: Vec<(String, String, String)> =
1651 select((workspace_id, language_name.as_ref().to_string(), worktree_id.to_usize(), relative_worktree_path))?;
1652
1653 Ok(toolchain.into_iter().next().and_then(|(name, path, raw_json)| Some(Toolchain {
1654 name: name.into(),
1655 path: path.into(),
1656 language_name,
1657 as_json: serde_json::Value::from_str(&raw_json).ok()?,
1658 })))
1659 })
1660 .await
1661 }
1662
1663 pub(crate) async fn toolchains(
1664 &self,
1665 workspace_id: WorkspaceId,
1666 ) -> Result<Vec<(Toolchain, WorktreeId, Arc<Path>)>> {
1667 self.write(move |this| {
1668 let mut select = this
1669 .select_bound(sql!(
1670 SELECT name, path, worktree_id, relative_worktree_path, language_name, raw_json FROM toolchains WHERE workspace_id = ?
1671 ))
1672 .context("select toolchains")?;
1673
1674 let toolchain: Vec<(String, String, u64, String, String, String)> =
1675 select(workspace_id)?;
1676
1677 Ok(toolchain.into_iter().filter_map(|(name, path, worktree_id, relative_worktree_path, language_name, raw_json)| Some((Toolchain {
1678 name: name.into(),
1679 path: path.into(),
1680 language_name: LanguageName::new(&language_name),
1681 as_json: serde_json::Value::from_str(&raw_json).ok()?,
1682 }, WorktreeId::from_proto(worktree_id), Arc::from(relative_worktree_path.as_ref())))).collect())
1683 })
1684 .await
1685 }
1686 pub async fn set_toolchain(
1687 &self,
1688 workspace_id: WorkspaceId,
1689 worktree_id: WorktreeId,
1690 relative_worktree_path: String,
1691 toolchain: Toolchain,
1692 ) -> Result<()> {
1693 log::debug!(
1694 "Setting toolchain for workspace, worktree: {worktree_id:?}, relative path: {relative_worktree_path:?}, toolchain: {}",
1695 toolchain.name
1696 );
1697 self.write(move |conn| {
1698 let mut insert = conn
1699 .exec_bound(sql!(
1700 INSERT INTO toolchains(workspace_id, worktree_id, relative_worktree_path, language_name, name, path, raw_json) VALUES (?, ?, ?, ?, ?, ?, ?)
1701 ON CONFLICT DO
1702 UPDATE SET
1703 name = ?5,
1704 path = ?6,
1705 raw_json = ?7
1706 ))
1707 .context("Preparing insertion")?;
1708
1709 insert((
1710 workspace_id,
1711 worktree_id.to_usize(),
1712 relative_worktree_path,
1713 toolchain.language_name.as_ref(),
1714 toolchain.name.as_ref(),
1715 toolchain.path.as_ref(),
1716 toolchain.as_json.to_string(),
1717 ))?;
1718
1719 Ok(())
1720 }).await
1721 }
1722}
1723
1724pub fn delete_unloaded_items(
1725 alive_items: Vec<ItemId>,
1726 workspace_id: WorkspaceId,
1727 table: &'static str,
1728 db: &ThreadSafeConnection,
1729 cx: &mut App,
1730) -> Task<Result<()>> {
1731 let db = db.clone();
1732 cx.spawn(async move |_| {
1733 let placeholders = alive_items
1734 .iter()
1735 .map(|_| "?")
1736 .collect::<Vec<&str>>()
1737 .join(", ");
1738
1739 let query = format!(
1740 "DELETE FROM {table} WHERE workspace_id = ? AND item_id NOT IN ({placeholders})"
1741 );
1742
1743 db.write(move |conn| {
1744 let mut statement = Statement::prepare(conn, query)?;
1745 let mut next_index = statement.bind(&workspace_id, 1)?;
1746 for id in alive_items {
1747 next_index = statement.bind(&id, next_index)?;
1748 }
1749 statement.exec()
1750 })
1751 .await
1752 })
1753}
1754
1755#[cfg(test)]
1756mod tests {
1757 use super::*;
1758 use crate::persistence::model::{
1759 SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
1760 };
1761 use gpui;
1762 use pretty_assertions::assert_eq;
1763 use remote::SshConnectionOptions;
1764 use std::{thread, time::Duration};
1765
1766 #[gpui::test]
1767 async fn test_breakpoints() {
1768 zlog::init_test();
1769
1770 let db = WorkspaceDb::open_test_db("test_breakpoints").await;
1771 let id = db.next_id().await.unwrap();
1772
1773 let path = Path::new("/tmp/test.rs");
1774
1775 let breakpoint = Breakpoint {
1776 position: 123,
1777 message: None,
1778 state: BreakpointState::Enabled,
1779 condition: None,
1780 hit_condition: None,
1781 };
1782
1783 let log_breakpoint = Breakpoint {
1784 position: 456,
1785 message: Some("Test log message".into()),
1786 state: BreakpointState::Enabled,
1787 condition: None,
1788 hit_condition: None,
1789 };
1790
1791 let disable_breakpoint = Breakpoint {
1792 position: 578,
1793 message: None,
1794 state: BreakpointState::Disabled,
1795 condition: None,
1796 hit_condition: None,
1797 };
1798
1799 let condition_breakpoint = Breakpoint {
1800 position: 789,
1801 message: None,
1802 state: BreakpointState::Enabled,
1803 condition: Some("x > 5".into()),
1804 hit_condition: None,
1805 };
1806
1807 let hit_condition_breakpoint = Breakpoint {
1808 position: 999,
1809 message: None,
1810 state: BreakpointState::Enabled,
1811 condition: None,
1812 hit_condition: Some(">= 3".into()),
1813 };
1814
1815 let workspace = SerializedWorkspace {
1816 id,
1817 paths: PathList::new(&["/tmp"]),
1818 location: SerializedWorkspaceLocation::Local,
1819 center_group: Default::default(),
1820 window_bounds: Default::default(),
1821 display: Default::default(),
1822 docks: Default::default(),
1823 centered_layout: false,
1824 breakpoints: {
1825 let mut map = collections::BTreeMap::default();
1826 map.insert(
1827 Arc::from(path),
1828 vec![
1829 SourceBreakpoint {
1830 row: breakpoint.position,
1831 path: Arc::from(path),
1832 message: breakpoint.message.clone(),
1833 state: breakpoint.state,
1834 condition: breakpoint.condition.clone(),
1835 hit_condition: breakpoint.hit_condition.clone(),
1836 },
1837 SourceBreakpoint {
1838 row: log_breakpoint.position,
1839 path: Arc::from(path),
1840 message: log_breakpoint.message.clone(),
1841 state: log_breakpoint.state,
1842 condition: log_breakpoint.condition.clone(),
1843 hit_condition: log_breakpoint.hit_condition.clone(),
1844 },
1845 SourceBreakpoint {
1846 row: disable_breakpoint.position,
1847 path: Arc::from(path),
1848 message: disable_breakpoint.message.clone(),
1849 state: disable_breakpoint.state,
1850 condition: disable_breakpoint.condition.clone(),
1851 hit_condition: disable_breakpoint.hit_condition.clone(),
1852 },
1853 SourceBreakpoint {
1854 row: condition_breakpoint.position,
1855 path: Arc::from(path),
1856 message: condition_breakpoint.message.clone(),
1857 state: condition_breakpoint.state,
1858 condition: condition_breakpoint.condition.clone(),
1859 hit_condition: condition_breakpoint.hit_condition.clone(),
1860 },
1861 SourceBreakpoint {
1862 row: hit_condition_breakpoint.position,
1863 path: Arc::from(path),
1864 message: hit_condition_breakpoint.message.clone(),
1865 state: hit_condition_breakpoint.state,
1866 condition: hit_condition_breakpoint.condition.clone(),
1867 hit_condition: hit_condition_breakpoint.hit_condition.clone(),
1868 },
1869 ],
1870 );
1871 map
1872 },
1873 session_id: None,
1874 window_id: None,
1875 user_toolchains: Default::default(),
1876 };
1877
1878 db.save_workspace(workspace.clone()).await;
1879
1880 let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
1881 let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(path)).unwrap();
1882
1883 assert_eq!(loaded_breakpoints.len(), 5);
1884
1885 // normal breakpoint
1886 assert_eq!(loaded_breakpoints[0].row, breakpoint.position);
1887 assert_eq!(loaded_breakpoints[0].message, breakpoint.message);
1888 assert_eq!(loaded_breakpoints[0].condition, breakpoint.condition);
1889 assert_eq!(
1890 loaded_breakpoints[0].hit_condition,
1891 breakpoint.hit_condition
1892 );
1893 assert_eq!(loaded_breakpoints[0].state, breakpoint.state);
1894 assert_eq!(loaded_breakpoints[0].path, Arc::from(path));
1895
1896 // enabled breakpoint
1897 assert_eq!(loaded_breakpoints[1].row, log_breakpoint.position);
1898 assert_eq!(loaded_breakpoints[1].message, log_breakpoint.message);
1899 assert_eq!(loaded_breakpoints[1].condition, log_breakpoint.condition);
1900 assert_eq!(
1901 loaded_breakpoints[1].hit_condition,
1902 log_breakpoint.hit_condition
1903 );
1904 assert_eq!(loaded_breakpoints[1].state, log_breakpoint.state);
1905 assert_eq!(loaded_breakpoints[1].path, Arc::from(path));
1906
1907 // disable breakpoint
1908 assert_eq!(loaded_breakpoints[2].row, disable_breakpoint.position);
1909 assert_eq!(loaded_breakpoints[2].message, disable_breakpoint.message);
1910 assert_eq!(
1911 loaded_breakpoints[2].condition,
1912 disable_breakpoint.condition
1913 );
1914 assert_eq!(
1915 loaded_breakpoints[2].hit_condition,
1916 disable_breakpoint.hit_condition
1917 );
1918 assert_eq!(loaded_breakpoints[2].state, disable_breakpoint.state);
1919 assert_eq!(loaded_breakpoints[2].path, Arc::from(path));
1920
1921 // condition breakpoint
1922 assert_eq!(loaded_breakpoints[3].row, condition_breakpoint.position);
1923 assert_eq!(loaded_breakpoints[3].message, condition_breakpoint.message);
1924 assert_eq!(
1925 loaded_breakpoints[3].condition,
1926 condition_breakpoint.condition
1927 );
1928 assert_eq!(
1929 loaded_breakpoints[3].hit_condition,
1930 condition_breakpoint.hit_condition
1931 );
1932 assert_eq!(loaded_breakpoints[3].state, condition_breakpoint.state);
1933 assert_eq!(loaded_breakpoints[3].path, Arc::from(path));
1934
1935 // hit condition breakpoint
1936 assert_eq!(loaded_breakpoints[4].row, hit_condition_breakpoint.position);
1937 assert_eq!(
1938 loaded_breakpoints[4].message,
1939 hit_condition_breakpoint.message
1940 );
1941 assert_eq!(
1942 loaded_breakpoints[4].condition,
1943 hit_condition_breakpoint.condition
1944 );
1945 assert_eq!(
1946 loaded_breakpoints[4].hit_condition,
1947 hit_condition_breakpoint.hit_condition
1948 );
1949 assert_eq!(loaded_breakpoints[4].state, hit_condition_breakpoint.state);
1950 assert_eq!(loaded_breakpoints[4].path, Arc::from(path));
1951 }
1952
1953 #[gpui::test]
1954 async fn test_remove_last_breakpoint() {
1955 zlog::init_test();
1956
1957 let db = WorkspaceDb::open_test_db("test_remove_last_breakpoint").await;
1958 let id = db.next_id().await.unwrap();
1959
1960 let singular_path = Path::new("/tmp/test_remove_last_breakpoint.rs");
1961
1962 let breakpoint_to_remove = Breakpoint {
1963 position: 100,
1964 message: None,
1965 state: BreakpointState::Enabled,
1966 condition: None,
1967 hit_condition: None,
1968 };
1969
1970 let workspace = SerializedWorkspace {
1971 id,
1972 paths: PathList::new(&["/tmp"]),
1973 location: SerializedWorkspaceLocation::Local,
1974 center_group: Default::default(),
1975 window_bounds: Default::default(),
1976 display: Default::default(),
1977 docks: Default::default(),
1978 centered_layout: false,
1979 breakpoints: {
1980 let mut map = collections::BTreeMap::default();
1981 map.insert(
1982 Arc::from(singular_path),
1983 vec![SourceBreakpoint {
1984 row: breakpoint_to_remove.position,
1985 path: Arc::from(singular_path),
1986 message: None,
1987 state: BreakpointState::Enabled,
1988 condition: None,
1989 hit_condition: None,
1990 }],
1991 );
1992 map
1993 },
1994 session_id: None,
1995 window_id: None,
1996 user_toolchains: Default::default(),
1997 };
1998
1999 db.save_workspace(workspace.clone()).await;
2000
2001 let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
2002 let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(singular_path)).unwrap();
2003
2004 assert_eq!(loaded_breakpoints.len(), 1);
2005 assert_eq!(loaded_breakpoints[0].row, breakpoint_to_remove.position);
2006 assert_eq!(loaded_breakpoints[0].message, breakpoint_to_remove.message);
2007 assert_eq!(
2008 loaded_breakpoints[0].condition,
2009 breakpoint_to_remove.condition
2010 );
2011 assert_eq!(
2012 loaded_breakpoints[0].hit_condition,
2013 breakpoint_to_remove.hit_condition
2014 );
2015 assert_eq!(loaded_breakpoints[0].state, breakpoint_to_remove.state);
2016 assert_eq!(loaded_breakpoints[0].path, Arc::from(singular_path));
2017
2018 let workspace_without_breakpoint = SerializedWorkspace {
2019 id,
2020 paths: PathList::new(&["/tmp"]),
2021 location: SerializedWorkspaceLocation::Local,
2022 center_group: Default::default(),
2023 window_bounds: Default::default(),
2024 display: Default::default(),
2025 docks: Default::default(),
2026 centered_layout: false,
2027 breakpoints: collections::BTreeMap::default(),
2028 session_id: None,
2029 window_id: None,
2030 user_toolchains: Default::default(),
2031 };
2032
2033 db.save_workspace(workspace_without_breakpoint.clone())
2034 .await;
2035
2036 let loaded_after_remove = db.workspace_for_roots(&["/tmp"]).unwrap();
2037 let empty_breakpoints = loaded_after_remove
2038 .breakpoints
2039 .get(&Arc::from(singular_path));
2040
2041 assert!(empty_breakpoints.is_none());
2042 }
2043
2044 #[gpui::test]
2045 async fn test_next_id_stability() {
2046 zlog::init_test();
2047
2048 let db = WorkspaceDb::open_test_db("test_next_id_stability").await;
2049
2050 db.write(|conn| {
2051 conn.migrate(
2052 "test_table",
2053 &[sql!(
2054 CREATE TABLE test_table(
2055 text TEXT,
2056 workspace_id INTEGER,
2057 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
2058 ON DELETE CASCADE
2059 ) STRICT;
2060 )],
2061 |_, _, _| false,
2062 )
2063 .unwrap();
2064 })
2065 .await;
2066
2067 let id = db.next_id().await.unwrap();
2068 // Assert the empty row got inserted
2069 assert_eq!(
2070 Some(id),
2071 db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
2072 SELECT workspace_id FROM workspaces WHERE workspace_id = ?
2073 ))
2074 .unwrap()(id)
2075 .unwrap()
2076 );
2077
2078 db.write(move |conn| {
2079 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2080 .unwrap()(("test-text-1", id))
2081 .unwrap()
2082 })
2083 .await;
2084
2085 let test_text_1 = db
2086 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2087 .unwrap()(1)
2088 .unwrap()
2089 .unwrap();
2090 assert_eq!(test_text_1, "test-text-1");
2091 }
2092
2093 #[gpui::test]
2094 async fn test_workspace_id_stability() {
2095 zlog::init_test();
2096
2097 let db = WorkspaceDb::open_test_db("test_workspace_id_stability").await;
2098
2099 db.write(|conn| {
2100 conn.migrate(
2101 "test_table",
2102 &[sql!(
2103 CREATE TABLE test_table(
2104 text TEXT,
2105 workspace_id INTEGER,
2106 FOREIGN KEY(workspace_id)
2107 REFERENCES workspaces(workspace_id)
2108 ON DELETE CASCADE
2109 ) STRICT;)],
2110 |_, _, _| false,
2111 )
2112 })
2113 .await
2114 .unwrap();
2115
2116 let mut workspace_1 = SerializedWorkspace {
2117 id: WorkspaceId(1),
2118 paths: PathList::new(&["/tmp", "/tmp2"]),
2119 location: SerializedWorkspaceLocation::Local,
2120 center_group: Default::default(),
2121 window_bounds: Default::default(),
2122 display: Default::default(),
2123 docks: Default::default(),
2124 centered_layout: false,
2125 breakpoints: Default::default(),
2126 session_id: None,
2127 window_id: None,
2128 user_toolchains: Default::default(),
2129 };
2130
2131 let workspace_2 = SerializedWorkspace {
2132 id: WorkspaceId(2),
2133 paths: PathList::new(&["/tmp"]),
2134 location: SerializedWorkspaceLocation::Local,
2135 center_group: Default::default(),
2136 window_bounds: Default::default(),
2137 display: Default::default(),
2138 docks: Default::default(),
2139 centered_layout: false,
2140 breakpoints: Default::default(),
2141 session_id: None,
2142 window_id: None,
2143 user_toolchains: Default::default(),
2144 };
2145
2146 db.save_workspace(workspace_1.clone()).await;
2147
2148 db.write(|conn| {
2149 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2150 .unwrap()(("test-text-1", 1))
2151 .unwrap();
2152 })
2153 .await;
2154
2155 db.save_workspace(workspace_2.clone()).await;
2156
2157 db.write(|conn| {
2158 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2159 .unwrap()(("test-text-2", 2))
2160 .unwrap();
2161 })
2162 .await;
2163
2164 workspace_1.paths = PathList::new(&["/tmp", "/tmp3"]);
2165 db.save_workspace(workspace_1.clone()).await;
2166 db.save_workspace(workspace_1).await;
2167 db.save_workspace(workspace_2).await;
2168
2169 let test_text_2 = db
2170 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2171 .unwrap()(2)
2172 .unwrap()
2173 .unwrap();
2174 assert_eq!(test_text_2, "test-text-2");
2175
2176 let test_text_1 = db
2177 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2178 .unwrap()(1)
2179 .unwrap()
2180 .unwrap();
2181 assert_eq!(test_text_1, "test-text-1");
2182 }
2183
2184 fn group(axis: Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
2185 SerializedPaneGroup::Group {
2186 axis: SerializedAxis(axis),
2187 flexes: None,
2188 children,
2189 }
2190 }
2191
2192 #[gpui::test]
2193 async fn test_full_workspace_serialization() {
2194 zlog::init_test();
2195
2196 let db = WorkspaceDb::open_test_db("test_full_workspace_serialization").await;
2197
2198 // -----------------
2199 // | 1,2 | 5,6 |
2200 // | - - - | |
2201 // | 3,4 | |
2202 // -----------------
2203 let center_group = group(
2204 Axis::Horizontal,
2205 vec![
2206 group(
2207 Axis::Vertical,
2208 vec![
2209 SerializedPaneGroup::Pane(SerializedPane::new(
2210 vec![
2211 SerializedItem::new("Terminal", 5, false, false),
2212 SerializedItem::new("Terminal", 6, true, false),
2213 ],
2214 false,
2215 0,
2216 )),
2217 SerializedPaneGroup::Pane(SerializedPane::new(
2218 vec![
2219 SerializedItem::new("Terminal", 7, true, false),
2220 SerializedItem::new("Terminal", 8, false, false),
2221 ],
2222 false,
2223 0,
2224 )),
2225 ],
2226 ),
2227 SerializedPaneGroup::Pane(SerializedPane::new(
2228 vec![
2229 SerializedItem::new("Terminal", 9, false, false),
2230 SerializedItem::new("Terminal", 10, true, false),
2231 ],
2232 false,
2233 0,
2234 )),
2235 ],
2236 );
2237
2238 let workspace = SerializedWorkspace {
2239 id: WorkspaceId(5),
2240 paths: PathList::new(&["/tmp", "/tmp2"]),
2241 location: SerializedWorkspaceLocation::Local,
2242 center_group,
2243 window_bounds: Default::default(),
2244 breakpoints: Default::default(),
2245 display: Default::default(),
2246 docks: Default::default(),
2247 centered_layout: false,
2248 session_id: None,
2249 window_id: Some(999),
2250 user_toolchains: Default::default(),
2251 };
2252
2253 db.save_workspace(workspace.clone()).await;
2254
2255 let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
2256 assert_eq!(workspace, round_trip_workspace.unwrap());
2257
2258 // Test guaranteed duplicate IDs
2259 db.save_workspace(workspace.clone()).await;
2260 db.save_workspace(workspace.clone()).await;
2261
2262 let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
2263 assert_eq!(workspace, round_trip_workspace.unwrap());
2264 }
2265
2266 #[gpui::test]
2267 async fn test_workspace_assignment() {
2268 zlog::init_test();
2269
2270 let db = WorkspaceDb::open_test_db("test_basic_functionality").await;
2271
2272 let workspace_1 = SerializedWorkspace {
2273 id: WorkspaceId(1),
2274 paths: PathList::new(&["/tmp", "/tmp2"]),
2275 location: SerializedWorkspaceLocation::Local,
2276 center_group: Default::default(),
2277 window_bounds: Default::default(),
2278 breakpoints: Default::default(),
2279 display: Default::default(),
2280 docks: Default::default(),
2281 centered_layout: false,
2282 session_id: None,
2283 window_id: Some(1),
2284 user_toolchains: Default::default(),
2285 };
2286
2287 let mut workspace_2 = SerializedWorkspace {
2288 id: WorkspaceId(2),
2289 paths: PathList::new(&["/tmp"]),
2290 location: SerializedWorkspaceLocation::Local,
2291 center_group: Default::default(),
2292 window_bounds: Default::default(),
2293 display: Default::default(),
2294 docks: Default::default(),
2295 centered_layout: false,
2296 breakpoints: Default::default(),
2297 session_id: None,
2298 window_id: Some(2),
2299 user_toolchains: Default::default(),
2300 };
2301
2302 db.save_workspace(workspace_1.clone()).await;
2303 db.save_workspace(workspace_2.clone()).await;
2304
2305 // Test that paths are treated as a set
2306 assert_eq!(
2307 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2308 workspace_1
2309 );
2310 assert_eq!(
2311 db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
2312 workspace_1
2313 );
2314
2315 // Make sure that other keys work
2316 assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
2317 assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
2318
2319 // Test 'mutate' case of updating a pre-existing id
2320 workspace_2.paths = PathList::new(&["/tmp", "/tmp2"]);
2321
2322 db.save_workspace(workspace_2.clone()).await;
2323 assert_eq!(
2324 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2325 workspace_2
2326 );
2327
2328 // Test other mechanism for mutating
2329 let mut workspace_3 = SerializedWorkspace {
2330 id: WorkspaceId(3),
2331 paths: PathList::new(&["/tmp2", "/tmp"]),
2332 location: SerializedWorkspaceLocation::Local,
2333 center_group: Default::default(),
2334 window_bounds: Default::default(),
2335 breakpoints: Default::default(),
2336 display: Default::default(),
2337 docks: Default::default(),
2338 centered_layout: false,
2339 session_id: None,
2340 window_id: Some(3),
2341 user_toolchains: Default::default(),
2342 };
2343
2344 db.save_workspace(workspace_3.clone()).await;
2345 assert_eq!(
2346 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2347 workspace_3
2348 );
2349
2350 // Make sure that updating paths differently also works
2351 workspace_3.paths = PathList::new(&["/tmp3", "/tmp4", "/tmp2"]);
2352 db.save_workspace(workspace_3.clone()).await;
2353 assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
2354 assert_eq!(
2355 db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
2356 .unwrap(),
2357 workspace_3
2358 );
2359 }
2360
2361 #[gpui::test]
2362 async fn test_session_workspaces() {
2363 zlog::init_test();
2364
2365 let db = WorkspaceDb::open_test_db("test_serializing_workspaces_session_id").await;
2366
2367 let workspace_1 = SerializedWorkspace {
2368 id: WorkspaceId(1),
2369 paths: PathList::new(&["/tmp1"]),
2370 location: SerializedWorkspaceLocation::Local,
2371 center_group: Default::default(),
2372 window_bounds: Default::default(),
2373 display: Default::default(),
2374 docks: Default::default(),
2375 centered_layout: false,
2376 breakpoints: Default::default(),
2377 session_id: Some("session-id-1".to_owned()),
2378 window_id: Some(10),
2379 user_toolchains: Default::default(),
2380 };
2381
2382 let workspace_2 = SerializedWorkspace {
2383 id: WorkspaceId(2),
2384 paths: PathList::new(&["/tmp2"]),
2385 location: SerializedWorkspaceLocation::Local,
2386 center_group: Default::default(),
2387 window_bounds: Default::default(),
2388 display: Default::default(),
2389 docks: Default::default(),
2390 centered_layout: false,
2391 breakpoints: Default::default(),
2392 session_id: Some("session-id-1".to_owned()),
2393 window_id: Some(20),
2394 user_toolchains: Default::default(),
2395 };
2396
2397 let workspace_3 = SerializedWorkspace {
2398 id: WorkspaceId(3),
2399 paths: PathList::new(&["/tmp3"]),
2400 location: SerializedWorkspaceLocation::Local,
2401 center_group: Default::default(),
2402 window_bounds: Default::default(),
2403 display: Default::default(),
2404 docks: Default::default(),
2405 centered_layout: false,
2406 breakpoints: Default::default(),
2407 session_id: Some("session-id-2".to_owned()),
2408 window_id: Some(30),
2409 user_toolchains: Default::default(),
2410 };
2411
2412 let workspace_4 = SerializedWorkspace {
2413 id: WorkspaceId(4),
2414 paths: PathList::new(&["/tmp4"]),
2415 location: SerializedWorkspaceLocation::Local,
2416 center_group: Default::default(),
2417 window_bounds: Default::default(),
2418 display: Default::default(),
2419 docks: Default::default(),
2420 centered_layout: false,
2421 breakpoints: Default::default(),
2422 session_id: None,
2423 window_id: None,
2424 user_toolchains: Default::default(),
2425 };
2426
2427 let connection_id = db
2428 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2429 host: "my-host".to_string(),
2430 port: Some(1234),
2431 ..Default::default()
2432 }))
2433 .await
2434 .unwrap();
2435
2436 let workspace_5 = SerializedWorkspace {
2437 id: WorkspaceId(5),
2438 paths: PathList::default(),
2439 location: SerializedWorkspaceLocation::Remote(
2440 db.remote_connection(connection_id).unwrap(),
2441 ),
2442 center_group: Default::default(),
2443 window_bounds: Default::default(),
2444 display: Default::default(),
2445 docks: Default::default(),
2446 centered_layout: false,
2447 breakpoints: Default::default(),
2448 session_id: Some("session-id-2".to_owned()),
2449 window_id: Some(50),
2450 user_toolchains: Default::default(),
2451 };
2452
2453 let workspace_6 = SerializedWorkspace {
2454 id: WorkspaceId(6),
2455 paths: PathList::new(&["/tmp6a", "/tmp6b", "/tmp6c"]),
2456 location: SerializedWorkspaceLocation::Local,
2457 center_group: Default::default(),
2458 window_bounds: Default::default(),
2459 breakpoints: Default::default(),
2460 display: Default::default(),
2461 docks: Default::default(),
2462 centered_layout: false,
2463 session_id: Some("session-id-3".to_owned()),
2464 window_id: Some(60),
2465 user_toolchains: Default::default(),
2466 };
2467
2468 db.save_workspace(workspace_1.clone()).await;
2469 thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
2470 db.save_workspace(workspace_2.clone()).await;
2471 db.save_workspace(workspace_3.clone()).await;
2472 thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
2473 db.save_workspace(workspace_4.clone()).await;
2474 db.save_workspace(workspace_5.clone()).await;
2475 db.save_workspace(workspace_6.clone()).await;
2476
2477 let locations = db.session_workspaces("session-id-1".to_owned()).unwrap();
2478 assert_eq!(locations.len(), 2);
2479 assert_eq!(locations[0].0, PathList::new(&["/tmp2"]));
2480 assert_eq!(locations[0].1, Some(20));
2481 assert_eq!(locations[1].0, PathList::new(&["/tmp1"]));
2482 assert_eq!(locations[1].1, Some(10));
2483
2484 let locations = db.session_workspaces("session-id-2".to_owned()).unwrap();
2485 assert_eq!(locations.len(), 2);
2486 assert_eq!(locations[0].0, PathList::default());
2487 assert_eq!(locations[0].1, Some(50));
2488 assert_eq!(locations[0].2, Some(connection_id));
2489 assert_eq!(locations[1].0, PathList::new(&["/tmp3"]));
2490 assert_eq!(locations[1].1, Some(30));
2491
2492 let locations = db.session_workspaces("session-id-3".to_owned()).unwrap();
2493 assert_eq!(locations.len(), 1);
2494 assert_eq!(
2495 locations[0].0,
2496 PathList::new(&["/tmp6a", "/tmp6b", "/tmp6c"]),
2497 );
2498 assert_eq!(locations[0].1, Some(60));
2499 }
2500
2501 fn default_workspace<P: AsRef<Path>>(
2502 paths: &[P],
2503 center_group: &SerializedPaneGroup,
2504 ) -> SerializedWorkspace {
2505 SerializedWorkspace {
2506 id: WorkspaceId(4),
2507 paths: PathList::new(paths),
2508 location: SerializedWorkspaceLocation::Local,
2509 center_group: center_group.clone(),
2510 window_bounds: Default::default(),
2511 display: Default::default(),
2512 docks: Default::default(),
2513 breakpoints: Default::default(),
2514 centered_layout: false,
2515 session_id: None,
2516 window_id: None,
2517 user_toolchains: Default::default(),
2518 }
2519 }
2520
2521 #[gpui::test]
2522 async fn test_last_session_workspace_locations() {
2523 let dir1 = tempfile::TempDir::with_prefix("dir1").unwrap();
2524 let dir2 = tempfile::TempDir::with_prefix("dir2").unwrap();
2525 let dir3 = tempfile::TempDir::with_prefix("dir3").unwrap();
2526 let dir4 = tempfile::TempDir::with_prefix("dir4").unwrap();
2527
2528 let db =
2529 WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces").await;
2530
2531 let workspaces = [
2532 (1, vec![dir1.path()], 9),
2533 (2, vec![dir2.path()], 5),
2534 (3, vec![dir3.path()], 8),
2535 (4, vec![dir4.path()], 2),
2536 (5, vec![dir1.path(), dir2.path(), dir3.path()], 3),
2537 (6, vec![dir4.path(), dir3.path(), dir2.path()], 4),
2538 ]
2539 .into_iter()
2540 .map(|(id, paths, window_id)| SerializedWorkspace {
2541 id: WorkspaceId(id),
2542 paths: PathList::new(paths.as_slice()),
2543 location: SerializedWorkspaceLocation::Local,
2544 center_group: Default::default(),
2545 window_bounds: Default::default(),
2546 display: Default::default(),
2547 docks: Default::default(),
2548 centered_layout: false,
2549 session_id: Some("one-session".to_owned()),
2550 breakpoints: Default::default(),
2551 window_id: Some(window_id),
2552 user_toolchains: Default::default(),
2553 })
2554 .collect::<Vec<_>>();
2555
2556 for workspace in workspaces.iter() {
2557 db.save_workspace(workspace.clone()).await;
2558 }
2559
2560 let stack = Some(Vec::from([
2561 WindowId::from(2), // Top
2562 WindowId::from(8),
2563 WindowId::from(5),
2564 WindowId::from(9),
2565 WindowId::from(3),
2566 WindowId::from(4), // Bottom
2567 ]));
2568
2569 let locations = db
2570 .last_session_workspace_locations("one-session", stack)
2571 .unwrap();
2572 assert_eq!(
2573 locations,
2574 [
2575 (
2576 SerializedWorkspaceLocation::Local,
2577 PathList::new(&[dir4.path()])
2578 ),
2579 (
2580 SerializedWorkspaceLocation::Local,
2581 PathList::new(&[dir3.path()])
2582 ),
2583 (
2584 SerializedWorkspaceLocation::Local,
2585 PathList::new(&[dir2.path()])
2586 ),
2587 (
2588 SerializedWorkspaceLocation::Local,
2589 PathList::new(&[dir1.path()])
2590 ),
2591 (
2592 SerializedWorkspaceLocation::Local,
2593 PathList::new(&[dir1.path(), dir2.path(), dir3.path()])
2594 ),
2595 (
2596 SerializedWorkspaceLocation::Local,
2597 PathList::new(&[dir4.path(), dir3.path(), dir2.path()])
2598 ),
2599 ]
2600 );
2601 }
2602
2603 #[gpui::test]
2604 async fn test_last_session_workspace_locations_remote() {
2605 let db =
2606 WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces_remote")
2607 .await;
2608
2609 let remote_connections = [
2610 ("host-1", "my-user-1"),
2611 ("host-2", "my-user-2"),
2612 ("host-3", "my-user-3"),
2613 ("host-4", "my-user-4"),
2614 ]
2615 .into_iter()
2616 .map(|(host, user)| async {
2617 let options = RemoteConnectionOptions::Ssh(SshConnectionOptions {
2618 host: host.to_string(),
2619 username: Some(user.to_string()),
2620 ..Default::default()
2621 });
2622 db.get_or_create_remote_connection(options.clone())
2623 .await
2624 .unwrap();
2625 options
2626 })
2627 .collect::<Vec<_>>();
2628
2629 let remote_connections = futures::future::join_all(remote_connections).await;
2630
2631 let workspaces = [
2632 (1, remote_connections[0].clone(), 9),
2633 (2, remote_connections[1].clone(), 5),
2634 (3, remote_connections[2].clone(), 8),
2635 (4, remote_connections[3].clone(), 2),
2636 ]
2637 .into_iter()
2638 .map(|(id, remote_connection, window_id)| SerializedWorkspace {
2639 id: WorkspaceId(id),
2640 paths: PathList::default(),
2641 location: SerializedWorkspaceLocation::Remote(remote_connection),
2642 center_group: Default::default(),
2643 window_bounds: Default::default(),
2644 display: Default::default(),
2645 docks: Default::default(),
2646 centered_layout: false,
2647 session_id: Some("one-session".to_owned()),
2648 breakpoints: Default::default(),
2649 window_id: Some(window_id),
2650 user_toolchains: Default::default(),
2651 })
2652 .collect::<Vec<_>>();
2653
2654 for workspace in workspaces.iter() {
2655 db.save_workspace(workspace.clone()).await;
2656 }
2657
2658 let stack = Some(Vec::from([
2659 WindowId::from(2), // Top
2660 WindowId::from(8),
2661 WindowId::from(5),
2662 WindowId::from(9), // Bottom
2663 ]));
2664
2665 let have = db
2666 .last_session_workspace_locations("one-session", stack)
2667 .unwrap();
2668 assert_eq!(have.len(), 4);
2669 assert_eq!(
2670 have[0],
2671 (
2672 SerializedWorkspaceLocation::Remote(remote_connections[3].clone()),
2673 PathList::default()
2674 )
2675 );
2676 assert_eq!(
2677 have[1],
2678 (
2679 SerializedWorkspaceLocation::Remote(remote_connections[2].clone()),
2680 PathList::default()
2681 )
2682 );
2683 assert_eq!(
2684 have[2],
2685 (
2686 SerializedWorkspaceLocation::Remote(remote_connections[1].clone()),
2687 PathList::default()
2688 )
2689 );
2690 assert_eq!(
2691 have[3],
2692 (
2693 SerializedWorkspaceLocation::Remote(remote_connections[0].clone()),
2694 PathList::default()
2695 )
2696 );
2697 }
2698
2699 #[gpui::test]
2700 async fn test_get_or_create_ssh_project() {
2701 let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project").await;
2702
2703 let host = "example.com".to_string();
2704 let port = Some(22_u16);
2705 let user = Some("user".to_string());
2706
2707 let connection_id = db
2708 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2709 host: host.clone(),
2710 port,
2711 username: user.clone(),
2712 ..Default::default()
2713 }))
2714 .await
2715 .unwrap();
2716
2717 // Test that calling the function again with the same parameters returns the same project
2718 let same_connection = db
2719 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2720 host: host.clone(),
2721 port,
2722 username: user.clone(),
2723 ..Default::default()
2724 }))
2725 .await
2726 .unwrap();
2727
2728 assert_eq!(connection_id, same_connection);
2729
2730 // Test with different parameters
2731 let host2 = "otherexample.com".to_string();
2732 let port2 = None;
2733 let user2 = Some("otheruser".to_string());
2734
2735 let different_connection = db
2736 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2737 host: host2.clone(),
2738 port: port2,
2739 username: user2.clone(),
2740 ..Default::default()
2741 }))
2742 .await
2743 .unwrap();
2744
2745 assert_ne!(connection_id, different_connection);
2746 }
2747
2748 #[gpui::test]
2749 async fn test_get_or_create_ssh_project_with_null_user() {
2750 let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project_with_null_user").await;
2751
2752 let (host, port, user) = ("example.com".to_string(), None, None);
2753
2754 let connection_id = db
2755 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2756 host: host.clone(),
2757 port,
2758 username: None,
2759 ..Default::default()
2760 }))
2761 .await
2762 .unwrap();
2763
2764 let same_connection_id = db
2765 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2766 host: host.clone(),
2767 port,
2768 username: user.clone(),
2769 ..Default::default()
2770 }))
2771 .await
2772 .unwrap();
2773
2774 assert_eq!(connection_id, same_connection_id);
2775 }
2776
2777 #[gpui::test]
2778 async fn test_get_remote_connections() {
2779 let db = WorkspaceDb::open_test_db("test_get_remote_connections").await;
2780
2781 let connections = [
2782 ("example.com".to_string(), None, None),
2783 (
2784 "anotherexample.com".to_string(),
2785 Some(123_u16),
2786 Some("user2".to_string()),
2787 ),
2788 ("yetanother.com".to_string(), Some(345_u16), None),
2789 ];
2790
2791 let mut ids = Vec::new();
2792 for (host, port, user) in connections.iter() {
2793 ids.push(
2794 db.get_or_create_remote_connection(RemoteConnectionOptions::Ssh(
2795 SshConnectionOptions {
2796 host: host.clone(),
2797 port: *port,
2798 username: user.clone(),
2799 ..Default::default()
2800 },
2801 ))
2802 .await
2803 .unwrap(),
2804 );
2805 }
2806
2807 let stored_connections = db.remote_connections().unwrap();
2808 assert_eq!(
2809 stored_connections,
2810 [
2811 (
2812 ids[0],
2813 RemoteConnectionOptions::Ssh(SshConnectionOptions {
2814 host: "example.com".into(),
2815 port: None,
2816 username: None,
2817 ..Default::default()
2818 }),
2819 ),
2820 (
2821 ids[1],
2822 RemoteConnectionOptions::Ssh(SshConnectionOptions {
2823 host: "anotherexample.com".into(),
2824 port: Some(123),
2825 username: Some("user2".into()),
2826 ..Default::default()
2827 }),
2828 ),
2829 (
2830 ids[2],
2831 RemoteConnectionOptions::Ssh(SshConnectionOptions {
2832 host: "yetanother.com".into(),
2833 port: Some(345),
2834 username: None,
2835 ..Default::default()
2836 }),
2837 ),
2838 ]
2839 .into_iter()
2840 .collect::<HashMap<_, _>>(),
2841 );
2842 }
2843
2844 #[gpui::test]
2845 async fn test_simple_split() {
2846 zlog::init_test();
2847
2848 let db = WorkspaceDb::open_test_db("simple_split").await;
2849
2850 // -----------------
2851 // | 1,2 | 5,6 |
2852 // | - - - | |
2853 // | 3,4 | |
2854 // -----------------
2855 let center_pane = group(
2856 Axis::Horizontal,
2857 vec![
2858 group(
2859 Axis::Vertical,
2860 vec![
2861 SerializedPaneGroup::Pane(SerializedPane::new(
2862 vec![
2863 SerializedItem::new("Terminal", 1, false, false),
2864 SerializedItem::new("Terminal", 2, true, false),
2865 ],
2866 false,
2867 0,
2868 )),
2869 SerializedPaneGroup::Pane(SerializedPane::new(
2870 vec![
2871 SerializedItem::new("Terminal", 4, false, false),
2872 SerializedItem::new("Terminal", 3, true, false),
2873 ],
2874 true,
2875 0,
2876 )),
2877 ],
2878 ),
2879 SerializedPaneGroup::Pane(SerializedPane::new(
2880 vec![
2881 SerializedItem::new("Terminal", 5, true, false),
2882 SerializedItem::new("Terminal", 6, false, false),
2883 ],
2884 false,
2885 0,
2886 )),
2887 ],
2888 );
2889
2890 let workspace = default_workspace(&["/tmp"], ¢er_pane);
2891
2892 db.save_workspace(workspace.clone()).await;
2893
2894 let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
2895
2896 assert_eq!(workspace.center_group, new_workspace.center_group);
2897 }
2898
2899 #[gpui::test]
2900 async fn test_cleanup_panes() {
2901 zlog::init_test();
2902
2903 let db = WorkspaceDb::open_test_db("test_cleanup_panes").await;
2904
2905 let center_pane = group(
2906 Axis::Horizontal,
2907 vec![
2908 group(
2909 Axis::Vertical,
2910 vec![
2911 SerializedPaneGroup::Pane(SerializedPane::new(
2912 vec![
2913 SerializedItem::new("Terminal", 1, false, false),
2914 SerializedItem::new("Terminal", 2, true, false),
2915 ],
2916 false,
2917 0,
2918 )),
2919 SerializedPaneGroup::Pane(SerializedPane::new(
2920 vec![
2921 SerializedItem::new("Terminal", 4, false, false),
2922 SerializedItem::new("Terminal", 3, true, false),
2923 ],
2924 true,
2925 0,
2926 )),
2927 ],
2928 ),
2929 SerializedPaneGroup::Pane(SerializedPane::new(
2930 vec![
2931 SerializedItem::new("Terminal", 5, false, false),
2932 SerializedItem::new("Terminal", 6, true, false),
2933 ],
2934 false,
2935 0,
2936 )),
2937 ],
2938 );
2939
2940 let id = &["/tmp"];
2941
2942 let mut workspace = default_workspace(id, ¢er_pane);
2943
2944 db.save_workspace(workspace.clone()).await;
2945
2946 workspace.center_group = group(
2947 Axis::Vertical,
2948 vec![
2949 SerializedPaneGroup::Pane(SerializedPane::new(
2950 vec![
2951 SerializedItem::new("Terminal", 1, false, false),
2952 SerializedItem::new("Terminal", 2, true, false),
2953 ],
2954 false,
2955 0,
2956 )),
2957 SerializedPaneGroup::Pane(SerializedPane::new(
2958 vec![
2959 SerializedItem::new("Terminal", 4, true, false),
2960 SerializedItem::new("Terminal", 3, false, false),
2961 ],
2962 true,
2963 0,
2964 )),
2965 ],
2966 );
2967
2968 db.save_workspace(workspace.clone()).await;
2969
2970 let new_workspace = db.workspace_for_roots(id).unwrap();
2971
2972 assert_eq!(workspace.center_group, new_workspace.center_group);
2973 }
2974}