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