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, rel_path::RelPath};
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 = u32::from(self.0) as _;
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 let Some(relative_path) = RelPath::unix(&relative_worktree_path).log_err() else {
919 continue;
920 };
921 if worktree_id != u64::MAX && relative_worktree_path != String::default() {
922 ToolchainScope::Subproject(
923 WorktreeId::from_usize(worktree_id as usize),
924 relative_path.into(),
925 )
926 } else {
927 ToolchainScope::Project
928 }
929 };
930 let Ok(as_json) = serde_json::from_str(&raw_json) else {
931 continue;
932 };
933 let toolchain = Toolchain {
934 name: SharedString::from(name),
935 path: SharedString::from(path),
936 language_name: LanguageName::from_proto(language_name),
937 as_json,
938 };
939 ret.entry(scope).or_default().insert(toolchain);
940 }
941
942 ret
943 }
944
945 /// Saves a workspace using the worktree roots. Will garbage collect any workspaces
946 /// that used this workspace previously
947 pub(crate) async fn save_workspace(&self, workspace: SerializedWorkspace) {
948 let paths = workspace.paths.serialize();
949 log::debug!("Saving workspace at location: {:?}", workspace.location);
950 self.write(move |conn| {
951 conn.with_savepoint("update_worktrees", || {
952 let remote_connection_id = match workspace.location.clone() {
953 SerializedWorkspaceLocation::Local => None,
954 SerializedWorkspaceLocation::Remote(connection_options) => {
955 Some(Self::get_or_create_remote_connection_internal(
956 conn,
957 connection_options
958 )?.0)
959 }
960 };
961
962 // Clear out panes and pane_groups
963 conn.exec_bound(sql!(
964 DELETE FROM pane_groups WHERE workspace_id = ?1;
965 DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)
966 .context("Clearing old panes")?;
967
968 conn.exec_bound(
969 sql!(
970 DELETE FROM breakpoints WHERE workspace_id = ?1;
971 )
972 )?(workspace.id).context("Clearing old breakpoints")?;
973
974 for (path, breakpoints) in workspace.breakpoints {
975 for bp in breakpoints {
976 let state = BreakpointStateWrapper::from(bp.state);
977 match conn.exec_bound(sql!(
978 INSERT INTO breakpoints (workspace_id, path, breakpoint_location, log_message, condition, hit_condition, state)
979 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7);))?
980
981 ((
982 workspace.id,
983 path.as_ref(),
984 bp.row,
985 bp.message,
986 bp.condition,
987 bp.hit_condition,
988 state,
989 )) {
990 Ok(_) => {
991 log::debug!("Stored breakpoint at row: {} in path: {}", bp.row, path.to_string_lossy())
992 }
993 Err(err) => {
994 log::error!("{err}");
995 continue;
996 }
997 }
998 }
999 }
1000
1001 conn.exec_bound(
1002 sql!(
1003 DELETE FROM user_toolchains WHERE workspace_id = ?1;
1004 )
1005 )?(workspace.id).context("Clearing old user toolchains")?;
1006
1007 for (scope, toolchains) in workspace.user_toolchains {
1008 for toolchain in toolchains {
1009 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));
1010 let (workspace_id, worktree_id, relative_worktree_path) = match scope {
1011 ToolchainScope::Subproject(worktree_id, ref path) => (Some(workspace.id), Some(worktree_id), Some(path.as_unix_str().to_owned())),
1012 ToolchainScope::Project => (Some(workspace.id), None, None),
1013 ToolchainScope::Global => (None, None, None),
1014 };
1015 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(),
1016 toolchain.language_name.as_ref().to_owned(), toolchain.name.to_string(), toolchain.path.to_string(), toolchain.as_json.to_string());
1017 if let Err(err) = conn.exec_bound(query)?(args) {
1018 log::error!("{err}");
1019 continue;
1020 }
1021 }
1022 }
1023
1024 conn.exec_bound(sql!(
1025 DELETE
1026 FROM workspaces
1027 WHERE
1028 workspace_id != ?1 AND
1029 paths IS ?2 AND
1030 remote_connection_id IS ?3
1031 ))?((
1032 workspace.id,
1033 paths.paths.clone(),
1034 remote_connection_id,
1035 ))
1036 .context("clearing out old locations")?;
1037
1038 // Upsert
1039 let query = sql!(
1040 INSERT INTO workspaces(
1041 workspace_id,
1042 paths,
1043 paths_order,
1044 remote_connection_id,
1045 left_dock_visible,
1046 left_dock_active_panel,
1047 left_dock_zoom,
1048 right_dock_visible,
1049 right_dock_active_panel,
1050 right_dock_zoom,
1051 bottom_dock_visible,
1052 bottom_dock_active_panel,
1053 bottom_dock_zoom,
1054 session_id,
1055 window_id,
1056 timestamp
1057 )
1058 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, CURRENT_TIMESTAMP)
1059 ON CONFLICT DO
1060 UPDATE SET
1061 paths = ?2,
1062 paths_order = ?3,
1063 remote_connection_id = ?4,
1064 left_dock_visible = ?5,
1065 left_dock_active_panel = ?6,
1066 left_dock_zoom = ?7,
1067 right_dock_visible = ?8,
1068 right_dock_active_panel = ?9,
1069 right_dock_zoom = ?10,
1070 bottom_dock_visible = ?11,
1071 bottom_dock_active_panel = ?12,
1072 bottom_dock_zoom = ?13,
1073 session_id = ?14,
1074 window_id = ?15,
1075 timestamp = CURRENT_TIMESTAMP
1076 );
1077 let mut prepared_query = conn.exec_bound(query)?;
1078 let args = (
1079 workspace.id,
1080 paths.paths.clone(),
1081 paths.order.clone(),
1082 remote_connection_id,
1083 workspace.docks,
1084 workspace.session_id,
1085 workspace.window_id,
1086 );
1087
1088 prepared_query(args).context("Updating workspace")?;
1089
1090 // Save center pane group
1091 Self::save_pane_group(conn, workspace.id, &workspace.center_group, None)
1092 .context("save pane group in save workspace")?;
1093
1094 Ok(())
1095 })
1096 .log_err();
1097 })
1098 .await;
1099 }
1100
1101 pub(crate) async fn get_or_create_remote_connection(
1102 &self,
1103 options: RemoteConnectionOptions,
1104 ) -> Result<RemoteConnectionId> {
1105 self.write(move |conn| Self::get_or_create_remote_connection_internal(conn, options))
1106 .await
1107 }
1108
1109 fn get_or_create_remote_connection_internal(
1110 this: &Connection,
1111 options: RemoteConnectionOptions,
1112 ) -> Result<RemoteConnectionId> {
1113 let kind;
1114 let user;
1115 let mut host = None;
1116 let mut port = None;
1117 let mut distro = None;
1118 match options {
1119 RemoteConnectionOptions::Ssh(options) => {
1120 kind = RemoteConnectionKind::Ssh;
1121 host = Some(options.host);
1122 port = options.port;
1123 user = options.username;
1124 }
1125 RemoteConnectionOptions::Wsl(options) => {
1126 kind = RemoteConnectionKind::Wsl;
1127 distro = Some(options.distro_name);
1128 user = options.user;
1129 }
1130 }
1131 Self::get_or_create_remote_connection_query(this, kind, host, port, user, distro)
1132 }
1133
1134 fn get_or_create_remote_connection_query(
1135 this: &Connection,
1136 kind: RemoteConnectionKind,
1137 host: Option<String>,
1138 port: Option<u16>,
1139 user: Option<String>,
1140 distro: Option<String>,
1141 ) -> Result<RemoteConnectionId> {
1142 if let Some(id) = this.select_row_bound(sql!(
1143 SELECT id
1144 FROM remote_connections
1145 WHERE
1146 kind IS ? AND
1147 host IS ? AND
1148 port IS ? AND
1149 user IS ? AND
1150 distro IS ?
1151 LIMIT 1
1152 ))?((
1153 kind.serialize(),
1154 host.clone(),
1155 port,
1156 user.clone(),
1157 distro.clone(),
1158 ))? {
1159 Ok(RemoteConnectionId(id))
1160 } else {
1161 let id = this.select_row_bound(sql!(
1162 INSERT INTO remote_connections (
1163 kind,
1164 host,
1165 port,
1166 user,
1167 distro
1168 ) VALUES (?1, ?2, ?3, ?4, ?5)
1169 RETURNING id
1170 ))?((kind.serialize(), host, port, user, distro))?
1171 .context("failed to insert remote project")?;
1172 Ok(RemoteConnectionId(id))
1173 }
1174 }
1175
1176 query! {
1177 pub async fn next_id() -> Result<WorkspaceId> {
1178 INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id
1179 }
1180 }
1181
1182 fn recent_workspaces(
1183 &self,
1184 ) -> Result<Vec<(WorkspaceId, PathList, Option<RemoteConnectionId>)>> {
1185 Ok(self
1186 .recent_workspaces_query()?
1187 .into_iter()
1188 .map(|(id, paths, order, remote_connection_id)| {
1189 (
1190 id,
1191 PathList::deserialize(&SerializedPathList { paths, order }),
1192 remote_connection_id.map(RemoteConnectionId),
1193 )
1194 })
1195 .collect())
1196 }
1197
1198 query! {
1199 fn recent_workspaces_query() -> Result<Vec<(WorkspaceId, String, String, Option<u64>)>> {
1200 SELECT workspace_id, paths, paths_order, remote_connection_id
1201 FROM workspaces
1202 WHERE
1203 paths IS NOT NULL OR
1204 remote_connection_id IS NOT NULL
1205 ORDER BY timestamp DESC
1206 }
1207 }
1208
1209 fn session_workspaces(
1210 &self,
1211 session_id: String,
1212 ) -> Result<Vec<(PathList, Option<u64>, Option<RemoteConnectionId>)>> {
1213 Ok(self
1214 .session_workspaces_query(session_id)?
1215 .into_iter()
1216 .map(|(paths, order, window_id, remote_connection_id)| {
1217 (
1218 PathList::deserialize(&SerializedPathList { paths, order }),
1219 window_id,
1220 remote_connection_id.map(RemoteConnectionId),
1221 )
1222 })
1223 .collect())
1224 }
1225
1226 query! {
1227 fn session_workspaces_query(session_id: String) -> Result<Vec<(String, String, Option<u64>, Option<u64>)>> {
1228 SELECT paths, paths_order, window_id, remote_connection_id
1229 FROM workspaces
1230 WHERE session_id = ?1
1231 ORDER BY timestamp DESC
1232 }
1233 }
1234
1235 query! {
1236 pub fn breakpoints_for_file(workspace_id: WorkspaceId, file_path: &Path) -> Result<Vec<Breakpoint>> {
1237 SELECT breakpoint_location
1238 FROM breakpoints
1239 WHERE workspace_id= ?1 AND path = ?2
1240 }
1241 }
1242
1243 query! {
1244 pub fn clear_breakpoints(file_path: &Path) -> Result<()> {
1245 DELETE FROM breakpoints
1246 WHERE file_path = ?2
1247 }
1248 }
1249
1250 fn remote_connections(&self) -> Result<HashMap<RemoteConnectionId, RemoteConnectionOptions>> {
1251 Ok(self.select(sql!(
1252 SELECT
1253 id, kind, host, port, user, distro
1254 FROM
1255 remote_connections
1256 ))?()?
1257 .into_iter()
1258 .filter_map(|(id, kind, host, port, user, distro)| {
1259 Some((
1260 RemoteConnectionId(id),
1261 Self::remote_connection_from_row(kind, host, port, user, distro)?,
1262 ))
1263 })
1264 .collect())
1265 }
1266
1267 pub(crate) fn remote_connection(
1268 &self,
1269 id: RemoteConnectionId,
1270 ) -> Result<RemoteConnectionOptions> {
1271 let (kind, host, port, user, distro) = self.select_row_bound(sql!(
1272 SELECT kind, host, port, user, distro
1273 FROM remote_connections
1274 WHERE id = ?
1275 ))?(id.0)?
1276 .context("no such remote connection")?;
1277 Self::remote_connection_from_row(kind, host, port, user, distro)
1278 .context("invalid remote_connection row")
1279 }
1280
1281 fn remote_connection_from_row(
1282 kind: String,
1283 host: Option<String>,
1284 port: Option<u16>,
1285 user: Option<String>,
1286 distro: Option<String>,
1287 ) -> Option<RemoteConnectionOptions> {
1288 match RemoteConnectionKind::deserialize(&kind)? {
1289 RemoteConnectionKind::Wsl => Some(RemoteConnectionOptions::Wsl(WslConnectionOptions {
1290 distro_name: distro?,
1291 user: user,
1292 })),
1293 RemoteConnectionKind::Ssh => Some(RemoteConnectionOptions::Ssh(SshConnectionOptions {
1294 host: host?,
1295 port,
1296 username: user,
1297 ..Default::default()
1298 })),
1299 }
1300 }
1301
1302 pub(crate) fn last_window(
1303 &self,
1304 ) -> anyhow::Result<(Option<Uuid>, Option<SerializedWindowBounds>)> {
1305 let mut prepared_query =
1306 self.select::<(Option<Uuid>, Option<SerializedWindowBounds>)>(sql!(
1307 SELECT
1308 display,
1309 window_state, window_x, window_y, window_width, window_height
1310 FROM workspaces
1311 WHERE paths
1312 IS NOT NULL
1313 ORDER BY timestamp DESC
1314 LIMIT 1
1315 ))?;
1316 let result = prepared_query()?;
1317 Ok(result.into_iter().next().unwrap_or((None, None)))
1318 }
1319
1320 query! {
1321 pub async fn delete_workspace_by_id(id: WorkspaceId) -> Result<()> {
1322 DELETE FROM workspaces
1323 WHERE workspace_id IS ?
1324 }
1325 }
1326
1327 // Returns the recent locations which are still valid on disk and deletes ones which no longer
1328 // exist.
1329 pub async fn recent_workspaces_on_disk(
1330 &self,
1331 ) -> Result<Vec<(WorkspaceId, SerializedWorkspaceLocation, PathList)>> {
1332 let mut result = Vec::new();
1333 let mut delete_tasks = Vec::new();
1334 let remote_connections = self.remote_connections()?;
1335
1336 for (id, paths, remote_connection_id) in self.recent_workspaces()? {
1337 if let Some(remote_connection_id) = remote_connection_id {
1338 if let Some(connection_options) = remote_connections.get(&remote_connection_id) {
1339 result.push((
1340 id,
1341 SerializedWorkspaceLocation::Remote(connection_options.clone()),
1342 paths,
1343 ));
1344 } else {
1345 delete_tasks.push(self.delete_workspace_by_id(id));
1346 }
1347 continue;
1348 }
1349
1350 let has_wsl_path = if cfg!(windows) {
1351 paths
1352 .paths()
1353 .iter()
1354 .any(|path| util::paths::WslPath::from_path(path).is_some())
1355 } else {
1356 false
1357 };
1358
1359 // Delete the workspace if any of the paths are WSL paths.
1360 // If a local workspace points to WSL, this check will cause us to wait for the
1361 // WSL VM and file server to boot up. This can block for many seconds.
1362 // Supported scenarios use remote workspaces.
1363 if !has_wsl_path
1364 && paths.paths().iter().all(|path| path.exists())
1365 && paths.paths().iter().any(|path| path.is_dir())
1366 {
1367 result.push((id, SerializedWorkspaceLocation::Local, paths));
1368 } else {
1369 delete_tasks.push(self.delete_workspace_by_id(id));
1370 }
1371 }
1372
1373 futures::future::join_all(delete_tasks).await;
1374 Ok(result)
1375 }
1376
1377 pub async fn last_workspace(&self) -> Result<Option<(SerializedWorkspaceLocation, PathList)>> {
1378 Ok(self
1379 .recent_workspaces_on_disk()
1380 .await?
1381 .into_iter()
1382 .next()
1383 .map(|(_, location, paths)| (location, paths)))
1384 }
1385
1386 // Returns the locations of the workspaces that were still opened when the last
1387 // session was closed (i.e. when Zed was quit).
1388 // If `last_session_window_order` is provided, the returned locations are ordered
1389 // according to that.
1390 pub fn last_session_workspace_locations(
1391 &self,
1392 last_session_id: &str,
1393 last_session_window_stack: Option<Vec<WindowId>>,
1394 ) -> Result<Vec<(SerializedWorkspaceLocation, PathList)>> {
1395 let mut workspaces = Vec::new();
1396
1397 for (paths, window_id, remote_connection_id) in
1398 self.session_workspaces(last_session_id.to_owned())?
1399 {
1400 if let Some(remote_connection_id) = remote_connection_id {
1401 workspaces.push((
1402 SerializedWorkspaceLocation::Remote(
1403 self.remote_connection(remote_connection_id)?,
1404 ),
1405 paths,
1406 window_id.map(WindowId::from),
1407 ));
1408 } else if paths.paths().iter().all(|path| path.exists())
1409 && paths.paths().iter().any(|path| path.is_dir())
1410 {
1411 workspaces.push((
1412 SerializedWorkspaceLocation::Local,
1413 paths,
1414 window_id.map(WindowId::from),
1415 ));
1416 }
1417 }
1418
1419 if let Some(stack) = last_session_window_stack {
1420 workspaces.sort_by_key(|(_, _, window_id)| {
1421 window_id
1422 .and_then(|id| stack.iter().position(|&order_id| order_id == id))
1423 .unwrap_or(usize::MAX)
1424 });
1425 }
1426
1427 Ok(workspaces
1428 .into_iter()
1429 .map(|(location, paths, _)| (location, paths))
1430 .collect::<Vec<_>>())
1431 }
1432
1433 fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
1434 Ok(self
1435 .get_pane_group(workspace_id, None)?
1436 .into_iter()
1437 .next()
1438 .unwrap_or_else(|| {
1439 SerializedPaneGroup::Pane(SerializedPane {
1440 active: true,
1441 children: vec![],
1442 pinned_count: 0,
1443 })
1444 }))
1445 }
1446
1447 fn get_pane_group(
1448 &self,
1449 workspace_id: WorkspaceId,
1450 group_id: Option<GroupId>,
1451 ) -> Result<Vec<SerializedPaneGroup>> {
1452 type GroupKey = (Option<GroupId>, WorkspaceId);
1453 type GroupOrPane = (
1454 Option<GroupId>,
1455 Option<SerializedAxis>,
1456 Option<PaneId>,
1457 Option<bool>,
1458 Option<usize>,
1459 Option<String>,
1460 );
1461 self.select_bound::<GroupKey, GroupOrPane>(sql!(
1462 SELECT group_id, axis, pane_id, active, pinned_count, flexes
1463 FROM (SELECT
1464 group_id,
1465 axis,
1466 NULL as pane_id,
1467 NULL as active,
1468 NULL as pinned_count,
1469 position,
1470 parent_group_id,
1471 workspace_id,
1472 flexes
1473 FROM pane_groups
1474 UNION
1475 SELECT
1476 NULL,
1477 NULL,
1478 center_panes.pane_id,
1479 panes.active as active,
1480 pinned_count,
1481 position,
1482 parent_group_id,
1483 panes.workspace_id as workspace_id,
1484 NULL
1485 FROM center_panes
1486 JOIN panes ON center_panes.pane_id = panes.pane_id)
1487 WHERE parent_group_id IS ? AND workspace_id = ?
1488 ORDER BY position
1489 ))?((group_id, workspace_id))?
1490 .into_iter()
1491 .map(|(group_id, axis, pane_id, active, pinned_count, flexes)| {
1492 let maybe_pane = maybe!({ Some((pane_id?, active?, pinned_count?)) });
1493 if let Some((group_id, axis)) = group_id.zip(axis) {
1494 let flexes = flexes
1495 .map(|flexes: String| serde_json::from_str::<Vec<f32>>(&flexes))
1496 .transpose()?;
1497
1498 Ok(SerializedPaneGroup::Group {
1499 axis,
1500 children: self.get_pane_group(workspace_id, Some(group_id))?,
1501 flexes,
1502 })
1503 } else if let Some((pane_id, active, pinned_count)) = maybe_pane {
1504 Ok(SerializedPaneGroup::Pane(SerializedPane::new(
1505 self.get_items(pane_id)?,
1506 active,
1507 pinned_count,
1508 )))
1509 } else {
1510 bail!("Pane Group Child was neither a pane group or a pane");
1511 }
1512 })
1513 // Filter out panes and pane groups which don't have any children or items
1514 .filter(|pane_group| match pane_group {
1515 Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
1516 Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
1517 _ => true,
1518 })
1519 .collect::<Result<_>>()
1520 }
1521
1522 fn save_pane_group(
1523 conn: &Connection,
1524 workspace_id: WorkspaceId,
1525 pane_group: &SerializedPaneGroup,
1526 parent: Option<(GroupId, usize)>,
1527 ) -> Result<()> {
1528 if parent.is_none() {
1529 log::debug!("Saving a pane group for workspace {workspace_id:?}");
1530 }
1531 match pane_group {
1532 SerializedPaneGroup::Group {
1533 axis,
1534 children,
1535 flexes,
1536 } => {
1537 let (parent_id, position) = parent.unzip();
1538
1539 let flex_string = flexes
1540 .as_ref()
1541 .map(|flexes| serde_json::json!(flexes).to_string());
1542
1543 let group_id = conn.select_row_bound::<_, i64>(sql!(
1544 INSERT INTO pane_groups(
1545 workspace_id,
1546 parent_group_id,
1547 position,
1548 axis,
1549 flexes
1550 )
1551 VALUES (?, ?, ?, ?, ?)
1552 RETURNING group_id
1553 ))?((
1554 workspace_id,
1555 parent_id,
1556 position,
1557 *axis,
1558 flex_string,
1559 ))?
1560 .context("Couldn't retrieve group_id from inserted pane_group")?;
1561
1562 for (position, group) in children.iter().enumerate() {
1563 Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))?
1564 }
1565
1566 Ok(())
1567 }
1568 SerializedPaneGroup::Pane(pane) => {
1569 Self::save_pane(conn, workspace_id, pane, parent)?;
1570 Ok(())
1571 }
1572 }
1573 }
1574
1575 fn save_pane(
1576 conn: &Connection,
1577 workspace_id: WorkspaceId,
1578 pane: &SerializedPane,
1579 parent: Option<(GroupId, usize)>,
1580 ) -> Result<PaneId> {
1581 let pane_id = conn.select_row_bound::<_, i64>(sql!(
1582 INSERT INTO panes(workspace_id, active, pinned_count)
1583 VALUES (?, ?, ?)
1584 RETURNING pane_id
1585 ))?((workspace_id, pane.active, pane.pinned_count))?
1586 .context("Could not retrieve inserted pane_id")?;
1587
1588 let (parent_id, order) = parent.unzip();
1589 conn.exec_bound(sql!(
1590 INSERT INTO center_panes(pane_id, parent_group_id, position)
1591 VALUES (?, ?, ?)
1592 ))?((pane_id, parent_id, order))?;
1593
1594 Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
1595
1596 Ok(pane_id)
1597 }
1598
1599 fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
1600 self.select_bound(sql!(
1601 SELECT kind, item_id, active, preview FROM items
1602 WHERE pane_id = ?
1603 ORDER BY position
1604 ))?(pane_id)
1605 }
1606
1607 fn save_items(
1608 conn: &Connection,
1609 workspace_id: WorkspaceId,
1610 pane_id: PaneId,
1611 items: &[SerializedItem],
1612 ) -> Result<()> {
1613 let mut insert = conn.exec_bound(sql!(
1614 INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active, preview) VALUES (?, ?, ?, ?, ?, ?, ?)
1615 )).context("Preparing insertion")?;
1616 for (position, item) in items.iter().enumerate() {
1617 insert((workspace_id, pane_id, position, item))?;
1618 }
1619
1620 Ok(())
1621 }
1622
1623 query! {
1624 pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> {
1625 UPDATE workspaces
1626 SET timestamp = CURRENT_TIMESTAMP
1627 WHERE workspace_id = ?
1628 }
1629 }
1630
1631 query! {
1632 pub(crate) async fn set_window_open_status(workspace_id: WorkspaceId, bounds: SerializedWindowBounds, display: Uuid) -> Result<()> {
1633 UPDATE workspaces
1634 SET window_state = ?2,
1635 window_x = ?3,
1636 window_y = ?4,
1637 window_width = ?5,
1638 window_height = ?6,
1639 display = ?7
1640 WHERE workspace_id = ?1
1641 }
1642 }
1643
1644 query! {
1645 pub(crate) async fn set_centered_layout(workspace_id: WorkspaceId, centered_layout: bool) -> Result<()> {
1646 UPDATE workspaces
1647 SET centered_layout = ?2
1648 WHERE workspace_id = ?1
1649 }
1650 }
1651
1652 query! {
1653 pub(crate) async fn set_session_id(workspace_id: WorkspaceId, session_id: Option<String>) -> Result<()> {
1654 UPDATE workspaces
1655 SET session_id = ?2
1656 WHERE workspace_id = ?1
1657 }
1658 }
1659
1660 pub async fn toolchain(
1661 &self,
1662 workspace_id: WorkspaceId,
1663 worktree_id: WorktreeId,
1664 relative_worktree_path: Arc<RelPath>,
1665 language_name: LanguageName,
1666 ) -> Result<Option<Toolchain>> {
1667 self.write(move |this| {
1668 let mut select = this
1669 .select_bound(sql!(
1670 SELECT
1671 name, path, raw_json
1672 FROM toolchains
1673 WHERE
1674 workspace_id = ? AND
1675 language_name = ? AND
1676 worktree_id = ? AND
1677 relative_worktree_path = ?
1678 ))
1679 .context("select toolchain")?;
1680
1681 let toolchain: Vec<(String, String, String)> = select((
1682 workspace_id,
1683 language_name.as_ref().to_string(),
1684 worktree_id.to_usize(),
1685 relative_worktree_path.as_unix_str().to_string(),
1686 ))?;
1687
1688 Ok(toolchain
1689 .into_iter()
1690 .next()
1691 .and_then(|(name, path, raw_json)| {
1692 Some(Toolchain {
1693 name: name.into(),
1694 path: path.into(),
1695 language_name,
1696 as_json: serde_json::Value::from_str(&raw_json).ok()?,
1697 })
1698 }))
1699 })
1700 .await
1701 }
1702
1703 pub(crate) async fn toolchains(
1704 &self,
1705 workspace_id: WorkspaceId,
1706 ) -> Result<Vec<(Toolchain, WorktreeId, Arc<RelPath>)>> {
1707 self.write(move |this| {
1708 let mut select = this
1709 .select_bound(sql!(
1710 SELECT
1711 name, path, worktree_id, relative_worktree_path, language_name, raw_json
1712 FROM toolchains
1713 WHERE workspace_id = ?
1714 ))
1715 .context("select toolchains")?;
1716
1717 let toolchain: Vec<(String, String, u64, String, String, String)> =
1718 select(workspace_id)?;
1719
1720 Ok(toolchain
1721 .into_iter()
1722 .filter_map(
1723 |(name, path, worktree_id, relative_worktree_path, language, json)| {
1724 Some((
1725 Toolchain {
1726 name: name.into(),
1727 path: path.into(),
1728 language_name: LanguageName::new(&language),
1729 as_json: serde_json::Value::from_str(&json).ok()?,
1730 },
1731 WorktreeId::from_proto(worktree_id),
1732 RelPath::from_proto(&relative_worktree_path).log_err()?,
1733 ))
1734 },
1735 )
1736 .collect())
1737 })
1738 .await
1739 }
1740
1741 pub async fn set_toolchain(
1742 &self,
1743 workspace_id: WorkspaceId,
1744 worktree_id: WorktreeId,
1745 relative_worktree_path: Arc<RelPath>,
1746 toolchain: Toolchain,
1747 ) -> Result<()> {
1748 log::debug!(
1749 "Setting toolchain for workspace, worktree: {worktree_id:?}, relative path: {relative_worktree_path:?}, toolchain: {}",
1750 toolchain.name
1751 );
1752 self.write(move |conn| {
1753 let mut insert = conn
1754 .exec_bound(sql!(
1755 INSERT INTO toolchains(workspace_id, worktree_id, relative_worktree_path, language_name, name, path, raw_json) VALUES (?, ?, ?, ?, ?, ?, ?)
1756 ON CONFLICT DO
1757 UPDATE SET
1758 name = ?5,
1759 path = ?6,
1760 raw_json = ?7
1761 ))
1762 .context("Preparing insertion")?;
1763
1764 insert((
1765 workspace_id,
1766 worktree_id.to_usize(),
1767 relative_worktree_path.as_unix_str(),
1768 toolchain.language_name.as_ref(),
1769 toolchain.name.as_ref(),
1770 toolchain.path.as_ref(),
1771 toolchain.as_json.to_string(),
1772 ))?;
1773
1774 Ok(())
1775 }).await
1776 }
1777}
1778
1779pub fn delete_unloaded_items(
1780 alive_items: Vec<ItemId>,
1781 workspace_id: WorkspaceId,
1782 table: &'static str,
1783 db: &ThreadSafeConnection,
1784 cx: &mut App,
1785) -> Task<Result<()>> {
1786 let db = db.clone();
1787 cx.spawn(async move |_| {
1788 let placeholders = alive_items
1789 .iter()
1790 .map(|_| "?")
1791 .collect::<Vec<&str>>()
1792 .join(", ");
1793
1794 let query = format!(
1795 "DELETE FROM {table} WHERE workspace_id = ? AND item_id NOT IN ({placeholders})"
1796 );
1797
1798 db.write(move |conn| {
1799 let mut statement = Statement::prepare(conn, query)?;
1800 let mut next_index = statement.bind(&workspace_id, 1)?;
1801 for id in alive_items {
1802 next_index = statement.bind(&id, next_index)?;
1803 }
1804 statement.exec()
1805 })
1806 .await
1807 })
1808}
1809
1810#[cfg(test)]
1811mod tests {
1812 use super::*;
1813 use crate::persistence::model::{
1814 SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
1815 };
1816 use gpui;
1817 use pretty_assertions::assert_eq;
1818 use remote::SshConnectionOptions;
1819 use std::{thread, time::Duration};
1820
1821 #[gpui::test]
1822 async fn test_breakpoints() {
1823 zlog::init_test();
1824
1825 let db = WorkspaceDb::open_test_db("test_breakpoints").await;
1826 let id = db.next_id().await.unwrap();
1827
1828 let path = Path::new("/tmp/test.rs");
1829
1830 let breakpoint = Breakpoint {
1831 position: 123,
1832 message: None,
1833 state: BreakpointState::Enabled,
1834 condition: None,
1835 hit_condition: None,
1836 };
1837
1838 let log_breakpoint = Breakpoint {
1839 position: 456,
1840 message: Some("Test log message".into()),
1841 state: BreakpointState::Enabled,
1842 condition: None,
1843 hit_condition: None,
1844 };
1845
1846 let disable_breakpoint = Breakpoint {
1847 position: 578,
1848 message: None,
1849 state: BreakpointState::Disabled,
1850 condition: None,
1851 hit_condition: None,
1852 };
1853
1854 let condition_breakpoint = Breakpoint {
1855 position: 789,
1856 message: None,
1857 state: BreakpointState::Enabled,
1858 condition: Some("x > 5".into()),
1859 hit_condition: None,
1860 };
1861
1862 let hit_condition_breakpoint = Breakpoint {
1863 position: 999,
1864 message: None,
1865 state: BreakpointState::Enabled,
1866 condition: None,
1867 hit_condition: Some(">= 3".into()),
1868 };
1869
1870 let workspace = SerializedWorkspace {
1871 id,
1872 paths: PathList::new(&["/tmp"]),
1873 location: SerializedWorkspaceLocation::Local,
1874 center_group: Default::default(),
1875 window_bounds: Default::default(),
1876 display: Default::default(),
1877 docks: Default::default(),
1878 centered_layout: false,
1879 breakpoints: {
1880 let mut map = collections::BTreeMap::default();
1881 map.insert(
1882 Arc::from(path),
1883 vec![
1884 SourceBreakpoint {
1885 row: breakpoint.position,
1886 path: Arc::from(path),
1887 message: breakpoint.message.clone(),
1888 state: breakpoint.state,
1889 condition: breakpoint.condition.clone(),
1890 hit_condition: breakpoint.hit_condition.clone(),
1891 },
1892 SourceBreakpoint {
1893 row: log_breakpoint.position,
1894 path: Arc::from(path),
1895 message: log_breakpoint.message.clone(),
1896 state: log_breakpoint.state,
1897 condition: log_breakpoint.condition.clone(),
1898 hit_condition: log_breakpoint.hit_condition.clone(),
1899 },
1900 SourceBreakpoint {
1901 row: disable_breakpoint.position,
1902 path: Arc::from(path),
1903 message: disable_breakpoint.message.clone(),
1904 state: disable_breakpoint.state,
1905 condition: disable_breakpoint.condition.clone(),
1906 hit_condition: disable_breakpoint.hit_condition.clone(),
1907 },
1908 SourceBreakpoint {
1909 row: condition_breakpoint.position,
1910 path: Arc::from(path),
1911 message: condition_breakpoint.message.clone(),
1912 state: condition_breakpoint.state,
1913 condition: condition_breakpoint.condition.clone(),
1914 hit_condition: condition_breakpoint.hit_condition.clone(),
1915 },
1916 SourceBreakpoint {
1917 row: hit_condition_breakpoint.position,
1918 path: Arc::from(path),
1919 message: hit_condition_breakpoint.message.clone(),
1920 state: hit_condition_breakpoint.state,
1921 condition: hit_condition_breakpoint.condition.clone(),
1922 hit_condition: hit_condition_breakpoint.hit_condition.clone(),
1923 },
1924 ],
1925 );
1926 map
1927 },
1928 session_id: None,
1929 window_id: None,
1930 user_toolchains: Default::default(),
1931 };
1932
1933 db.save_workspace(workspace.clone()).await;
1934
1935 let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
1936 let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(path)).unwrap();
1937
1938 assert_eq!(loaded_breakpoints.len(), 5);
1939
1940 // normal breakpoint
1941 assert_eq!(loaded_breakpoints[0].row, breakpoint.position);
1942 assert_eq!(loaded_breakpoints[0].message, breakpoint.message);
1943 assert_eq!(loaded_breakpoints[0].condition, breakpoint.condition);
1944 assert_eq!(
1945 loaded_breakpoints[0].hit_condition,
1946 breakpoint.hit_condition
1947 );
1948 assert_eq!(loaded_breakpoints[0].state, breakpoint.state);
1949 assert_eq!(loaded_breakpoints[0].path, Arc::from(path));
1950
1951 // enabled breakpoint
1952 assert_eq!(loaded_breakpoints[1].row, log_breakpoint.position);
1953 assert_eq!(loaded_breakpoints[1].message, log_breakpoint.message);
1954 assert_eq!(loaded_breakpoints[1].condition, log_breakpoint.condition);
1955 assert_eq!(
1956 loaded_breakpoints[1].hit_condition,
1957 log_breakpoint.hit_condition
1958 );
1959 assert_eq!(loaded_breakpoints[1].state, log_breakpoint.state);
1960 assert_eq!(loaded_breakpoints[1].path, Arc::from(path));
1961
1962 // disable breakpoint
1963 assert_eq!(loaded_breakpoints[2].row, disable_breakpoint.position);
1964 assert_eq!(loaded_breakpoints[2].message, disable_breakpoint.message);
1965 assert_eq!(
1966 loaded_breakpoints[2].condition,
1967 disable_breakpoint.condition
1968 );
1969 assert_eq!(
1970 loaded_breakpoints[2].hit_condition,
1971 disable_breakpoint.hit_condition
1972 );
1973 assert_eq!(loaded_breakpoints[2].state, disable_breakpoint.state);
1974 assert_eq!(loaded_breakpoints[2].path, Arc::from(path));
1975
1976 // condition breakpoint
1977 assert_eq!(loaded_breakpoints[3].row, condition_breakpoint.position);
1978 assert_eq!(loaded_breakpoints[3].message, condition_breakpoint.message);
1979 assert_eq!(
1980 loaded_breakpoints[3].condition,
1981 condition_breakpoint.condition
1982 );
1983 assert_eq!(
1984 loaded_breakpoints[3].hit_condition,
1985 condition_breakpoint.hit_condition
1986 );
1987 assert_eq!(loaded_breakpoints[3].state, condition_breakpoint.state);
1988 assert_eq!(loaded_breakpoints[3].path, Arc::from(path));
1989
1990 // hit condition breakpoint
1991 assert_eq!(loaded_breakpoints[4].row, hit_condition_breakpoint.position);
1992 assert_eq!(
1993 loaded_breakpoints[4].message,
1994 hit_condition_breakpoint.message
1995 );
1996 assert_eq!(
1997 loaded_breakpoints[4].condition,
1998 hit_condition_breakpoint.condition
1999 );
2000 assert_eq!(
2001 loaded_breakpoints[4].hit_condition,
2002 hit_condition_breakpoint.hit_condition
2003 );
2004 assert_eq!(loaded_breakpoints[4].state, hit_condition_breakpoint.state);
2005 assert_eq!(loaded_breakpoints[4].path, Arc::from(path));
2006 }
2007
2008 #[gpui::test]
2009 async fn test_remove_last_breakpoint() {
2010 zlog::init_test();
2011
2012 let db = WorkspaceDb::open_test_db("test_remove_last_breakpoint").await;
2013 let id = db.next_id().await.unwrap();
2014
2015 let singular_path = Path::new("/tmp/test_remove_last_breakpoint.rs");
2016
2017 let breakpoint_to_remove = Breakpoint {
2018 position: 100,
2019 message: None,
2020 state: BreakpointState::Enabled,
2021 condition: None,
2022 hit_condition: None,
2023 };
2024
2025 let workspace = SerializedWorkspace {
2026 id,
2027 paths: PathList::new(&["/tmp"]),
2028 location: SerializedWorkspaceLocation::Local,
2029 center_group: Default::default(),
2030 window_bounds: Default::default(),
2031 display: Default::default(),
2032 docks: Default::default(),
2033 centered_layout: false,
2034 breakpoints: {
2035 let mut map = collections::BTreeMap::default();
2036 map.insert(
2037 Arc::from(singular_path),
2038 vec![SourceBreakpoint {
2039 row: breakpoint_to_remove.position,
2040 path: Arc::from(singular_path),
2041 message: None,
2042 state: BreakpointState::Enabled,
2043 condition: None,
2044 hit_condition: None,
2045 }],
2046 );
2047 map
2048 },
2049 session_id: None,
2050 window_id: None,
2051 user_toolchains: Default::default(),
2052 };
2053
2054 db.save_workspace(workspace.clone()).await;
2055
2056 let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
2057 let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(singular_path)).unwrap();
2058
2059 assert_eq!(loaded_breakpoints.len(), 1);
2060 assert_eq!(loaded_breakpoints[0].row, breakpoint_to_remove.position);
2061 assert_eq!(loaded_breakpoints[0].message, breakpoint_to_remove.message);
2062 assert_eq!(
2063 loaded_breakpoints[0].condition,
2064 breakpoint_to_remove.condition
2065 );
2066 assert_eq!(
2067 loaded_breakpoints[0].hit_condition,
2068 breakpoint_to_remove.hit_condition
2069 );
2070 assert_eq!(loaded_breakpoints[0].state, breakpoint_to_remove.state);
2071 assert_eq!(loaded_breakpoints[0].path, Arc::from(singular_path));
2072
2073 let workspace_without_breakpoint = SerializedWorkspace {
2074 id,
2075 paths: PathList::new(&["/tmp"]),
2076 location: SerializedWorkspaceLocation::Local,
2077 center_group: Default::default(),
2078 window_bounds: Default::default(),
2079 display: Default::default(),
2080 docks: Default::default(),
2081 centered_layout: false,
2082 breakpoints: collections::BTreeMap::default(),
2083 session_id: None,
2084 window_id: None,
2085 user_toolchains: Default::default(),
2086 };
2087
2088 db.save_workspace(workspace_without_breakpoint.clone())
2089 .await;
2090
2091 let loaded_after_remove = db.workspace_for_roots(&["/tmp"]).unwrap();
2092 let empty_breakpoints = loaded_after_remove
2093 .breakpoints
2094 .get(&Arc::from(singular_path));
2095
2096 assert!(empty_breakpoints.is_none());
2097 }
2098
2099 #[gpui::test]
2100 async fn test_next_id_stability() {
2101 zlog::init_test();
2102
2103 let db = WorkspaceDb::open_test_db("test_next_id_stability").await;
2104
2105 db.write(|conn| {
2106 conn.migrate(
2107 "test_table",
2108 &[sql!(
2109 CREATE TABLE test_table(
2110 text TEXT,
2111 workspace_id INTEGER,
2112 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
2113 ON DELETE CASCADE
2114 ) STRICT;
2115 )],
2116 |_, _, _| false,
2117 )
2118 .unwrap();
2119 })
2120 .await;
2121
2122 let id = db.next_id().await.unwrap();
2123 // Assert the empty row got inserted
2124 assert_eq!(
2125 Some(id),
2126 db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
2127 SELECT workspace_id FROM workspaces WHERE workspace_id = ?
2128 ))
2129 .unwrap()(id)
2130 .unwrap()
2131 );
2132
2133 db.write(move |conn| {
2134 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2135 .unwrap()(("test-text-1", id))
2136 .unwrap()
2137 })
2138 .await;
2139
2140 let test_text_1 = db
2141 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2142 .unwrap()(1)
2143 .unwrap()
2144 .unwrap();
2145 assert_eq!(test_text_1, "test-text-1");
2146 }
2147
2148 #[gpui::test]
2149 async fn test_workspace_id_stability() {
2150 zlog::init_test();
2151
2152 let db = WorkspaceDb::open_test_db("test_workspace_id_stability").await;
2153
2154 db.write(|conn| {
2155 conn.migrate(
2156 "test_table",
2157 &[sql!(
2158 CREATE TABLE test_table(
2159 text TEXT,
2160 workspace_id INTEGER,
2161 FOREIGN KEY(workspace_id)
2162 REFERENCES workspaces(workspace_id)
2163 ON DELETE CASCADE
2164 ) STRICT;)],
2165 |_, _, _| false,
2166 )
2167 })
2168 .await
2169 .unwrap();
2170
2171 let mut workspace_1 = SerializedWorkspace {
2172 id: WorkspaceId(1),
2173 paths: PathList::new(&["/tmp", "/tmp2"]),
2174 location: SerializedWorkspaceLocation::Local,
2175 center_group: Default::default(),
2176 window_bounds: Default::default(),
2177 display: Default::default(),
2178 docks: Default::default(),
2179 centered_layout: false,
2180 breakpoints: Default::default(),
2181 session_id: None,
2182 window_id: None,
2183 user_toolchains: Default::default(),
2184 };
2185
2186 let workspace_2 = SerializedWorkspace {
2187 id: WorkspaceId(2),
2188 paths: PathList::new(&["/tmp"]),
2189 location: SerializedWorkspaceLocation::Local,
2190 center_group: Default::default(),
2191 window_bounds: Default::default(),
2192 display: Default::default(),
2193 docks: Default::default(),
2194 centered_layout: false,
2195 breakpoints: Default::default(),
2196 session_id: None,
2197 window_id: None,
2198 user_toolchains: Default::default(),
2199 };
2200
2201 db.save_workspace(workspace_1.clone()).await;
2202
2203 db.write(|conn| {
2204 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2205 .unwrap()(("test-text-1", 1))
2206 .unwrap();
2207 })
2208 .await;
2209
2210 db.save_workspace(workspace_2.clone()).await;
2211
2212 db.write(|conn| {
2213 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2214 .unwrap()(("test-text-2", 2))
2215 .unwrap();
2216 })
2217 .await;
2218
2219 workspace_1.paths = PathList::new(&["/tmp", "/tmp3"]);
2220 db.save_workspace(workspace_1.clone()).await;
2221 db.save_workspace(workspace_1).await;
2222 db.save_workspace(workspace_2).await;
2223
2224 let test_text_2 = db
2225 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2226 .unwrap()(2)
2227 .unwrap()
2228 .unwrap();
2229 assert_eq!(test_text_2, "test-text-2");
2230
2231 let test_text_1 = db
2232 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2233 .unwrap()(1)
2234 .unwrap()
2235 .unwrap();
2236 assert_eq!(test_text_1, "test-text-1");
2237 }
2238
2239 fn group(axis: Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
2240 SerializedPaneGroup::Group {
2241 axis: SerializedAxis(axis),
2242 flexes: None,
2243 children,
2244 }
2245 }
2246
2247 #[gpui::test]
2248 async fn test_full_workspace_serialization() {
2249 zlog::init_test();
2250
2251 let db = WorkspaceDb::open_test_db("test_full_workspace_serialization").await;
2252
2253 // -----------------
2254 // | 1,2 | 5,6 |
2255 // | - - - | |
2256 // | 3,4 | |
2257 // -----------------
2258 let center_group = group(
2259 Axis::Horizontal,
2260 vec![
2261 group(
2262 Axis::Vertical,
2263 vec![
2264 SerializedPaneGroup::Pane(SerializedPane::new(
2265 vec![
2266 SerializedItem::new("Terminal", 5, false, false),
2267 SerializedItem::new("Terminal", 6, true, false),
2268 ],
2269 false,
2270 0,
2271 )),
2272 SerializedPaneGroup::Pane(SerializedPane::new(
2273 vec![
2274 SerializedItem::new("Terminal", 7, true, false),
2275 SerializedItem::new("Terminal", 8, false, false),
2276 ],
2277 false,
2278 0,
2279 )),
2280 ],
2281 ),
2282 SerializedPaneGroup::Pane(SerializedPane::new(
2283 vec![
2284 SerializedItem::new("Terminal", 9, false, false),
2285 SerializedItem::new("Terminal", 10, true, false),
2286 ],
2287 false,
2288 0,
2289 )),
2290 ],
2291 );
2292
2293 let workspace = SerializedWorkspace {
2294 id: WorkspaceId(5),
2295 paths: PathList::new(&["/tmp", "/tmp2"]),
2296 location: SerializedWorkspaceLocation::Local,
2297 center_group,
2298 window_bounds: Default::default(),
2299 breakpoints: Default::default(),
2300 display: Default::default(),
2301 docks: Default::default(),
2302 centered_layout: false,
2303 session_id: None,
2304 window_id: Some(999),
2305 user_toolchains: Default::default(),
2306 };
2307
2308 db.save_workspace(workspace.clone()).await;
2309
2310 let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
2311 assert_eq!(workspace, round_trip_workspace.unwrap());
2312
2313 // Test guaranteed duplicate IDs
2314 db.save_workspace(workspace.clone()).await;
2315 db.save_workspace(workspace.clone()).await;
2316
2317 let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
2318 assert_eq!(workspace, round_trip_workspace.unwrap());
2319 }
2320
2321 #[gpui::test]
2322 async fn test_workspace_assignment() {
2323 zlog::init_test();
2324
2325 let db = WorkspaceDb::open_test_db("test_basic_functionality").await;
2326
2327 let workspace_1 = SerializedWorkspace {
2328 id: WorkspaceId(1),
2329 paths: PathList::new(&["/tmp", "/tmp2"]),
2330 location: SerializedWorkspaceLocation::Local,
2331 center_group: Default::default(),
2332 window_bounds: Default::default(),
2333 breakpoints: Default::default(),
2334 display: Default::default(),
2335 docks: Default::default(),
2336 centered_layout: false,
2337 session_id: None,
2338 window_id: Some(1),
2339 user_toolchains: Default::default(),
2340 };
2341
2342 let mut workspace_2 = SerializedWorkspace {
2343 id: WorkspaceId(2),
2344 paths: PathList::new(&["/tmp"]),
2345 location: SerializedWorkspaceLocation::Local,
2346 center_group: Default::default(),
2347 window_bounds: Default::default(),
2348 display: Default::default(),
2349 docks: Default::default(),
2350 centered_layout: false,
2351 breakpoints: Default::default(),
2352 session_id: None,
2353 window_id: Some(2),
2354 user_toolchains: Default::default(),
2355 };
2356
2357 db.save_workspace(workspace_1.clone()).await;
2358 db.save_workspace(workspace_2.clone()).await;
2359
2360 // Test that paths are treated as a set
2361 assert_eq!(
2362 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2363 workspace_1
2364 );
2365 assert_eq!(
2366 db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
2367 workspace_1
2368 );
2369
2370 // Make sure that other keys work
2371 assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
2372 assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
2373
2374 // Test 'mutate' case of updating a pre-existing id
2375 workspace_2.paths = PathList::new(&["/tmp", "/tmp2"]);
2376
2377 db.save_workspace(workspace_2.clone()).await;
2378 assert_eq!(
2379 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2380 workspace_2
2381 );
2382
2383 // Test other mechanism for mutating
2384 let mut workspace_3 = SerializedWorkspace {
2385 id: WorkspaceId(3),
2386 paths: PathList::new(&["/tmp2", "/tmp"]),
2387 location: SerializedWorkspaceLocation::Local,
2388 center_group: Default::default(),
2389 window_bounds: Default::default(),
2390 breakpoints: Default::default(),
2391 display: Default::default(),
2392 docks: Default::default(),
2393 centered_layout: false,
2394 session_id: None,
2395 window_id: Some(3),
2396 user_toolchains: Default::default(),
2397 };
2398
2399 db.save_workspace(workspace_3.clone()).await;
2400 assert_eq!(
2401 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2402 workspace_3
2403 );
2404
2405 // Make sure that updating paths differently also works
2406 workspace_3.paths = PathList::new(&["/tmp3", "/tmp4", "/tmp2"]);
2407 db.save_workspace(workspace_3.clone()).await;
2408 assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
2409 assert_eq!(
2410 db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
2411 .unwrap(),
2412 workspace_3
2413 );
2414 }
2415
2416 #[gpui::test]
2417 async fn test_session_workspaces() {
2418 zlog::init_test();
2419
2420 let db = WorkspaceDb::open_test_db("test_serializing_workspaces_session_id").await;
2421
2422 let workspace_1 = SerializedWorkspace {
2423 id: WorkspaceId(1),
2424 paths: PathList::new(&["/tmp1"]),
2425 location: SerializedWorkspaceLocation::Local,
2426 center_group: Default::default(),
2427 window_bounds: Default::default(),
2428 display: Default::default(),
2429 docks: Default::default(),
2430 centered_layout: false,
2431 breakpoints: Default::default(),
2432 session_id: Some("session-id-1".to_owned()),
2433 window_id: Some(10),
2434 user_toolchains: Default::default(),
2435 };
2436
2437 let workspace_2 = SerializedWorkspace {
2438 id: WorkspaceId(2),
2439 paths: PathList::new(&["/tmp2"]),
2440 location: SerializedWorkspaceLocation::Local,
2441 center_group: Default::default(),
2442 window_bounds: Default::default(),
2443 display: Default::default(),
2444 docks: Default::default(),
2445 centered_layout: false,
2446 breakpoints: Default::default(),
2447 session_id: Some("session-id-1".to_owned()),
2448 window_id: Some(20),
2449 user_toolchains: Default::default(),
2450 };
2451
2452 let workspace_3 = SerializedWorkspace {
2453 id: WorkspaceId(3),
2454 paths: PathList::new(&["/tmp3"]),
2455 location: SerializedWorkspaceLocation::Local,
2456 center_group: Default::default(),
2457 window_bounds: Default::default(),
2458 display: Default::default(),
2459 docks: Default::default(),
2460 centered_layout: false,
2461 breakpoints: Default::default(),
2462 session_id: Some("session-id-2".to_owned()),
2463 window_id: Some(30),
2464 user_toolchains: Default::default(),
2465 };
2466
2467 let workspace_4 = SerializedWorkspace {
2468 id: WorkspaceId(4),
2469 paths: PathList::new(&["/tmp4"]),
2470 location: SerializedWorkspaceLocation::Local,
2471 center_group: Default::default(),
2472 window_bounds: Default::default(),
2473 display: Default::default(),
2474 docks: Default::default(),
2475 centered_layout: false,
2476 breakpoints: Default::default(),
2477 session_id: None,
2478 window_id: None,
2479 user_toolchains: Default::default(),
2480 };
2481
2482 let connection_id = db
2483 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2484 host: "my-host".to_string(),
2485 port: Some(1234),
2486 ..Default::default()
2487 }))
2488 .await
2489 .unwrap();
2490
2491 let workspace_5 = SerializedWorkspace {
2492 id: WorkspaceId(5),
2493 paths: PathList::default(),
2494 location: SerializedWorkspaceLocation::Remote(
2495 db.remote_connection(connection_id).unwrap(),
2496 ),
2497 center_group: Default::default(),
2498 window_bounds: Default::default(),
2499 display: Default::default(),
2500 docks: Default::default(),
2501 centered_layout: false,
2502 breakpoints: Default::default(),
2503 session_id: Some("session-id-2".to_owned()),
2504 window_id: Some(50),
2505 user_toolchains: Default::default(),
2506 };
2507
2508 let workspace_6 = SerializedWorkspace {
2509 id: WorkspaceId(6),
2510 paths: PathList::new(&["/tmp6c", "/tmp6b", "/tmp6a"]),
2511 location: SerializedWorkspaceLocation::Local,
2512 center_group: Default::default(),
2513 window_bounds: Default::default(),
2514 breakpoints: Default::default(),
2515 display: Default::default(),
2516 docks: Default::default(),
2517 centered_layout: false,
2518 session_id: Some("session-id-3".to_owned()),
2519 window_id: Some(60),
2520 user_toolchains: Default::default(),
2521 };
2522
2523 db.save_workspace(workspace_1.clone()).await;
2524 thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
2525 db.save_workspace(workspace_2.clone()).await;
2526 db.save_workspace(workspace_3.clone()).await;
2527 thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
2528 db.save_workspace(workspace_4.clone()).await;
2529 db.save_workspace(workspace_5.clone()).await;
2530 db.save_workspace(workspace_6.clone()).await;
2531
2532 let locations = db.session_workspaces("session-id-1".to_owned()).unwrap();
2533 assert_eq!(locations.len(), 2);
2534 assert_eq!(locations[0].0, PathList::new(&["/tmp2"]));
2535 assert_eq!(locations[0].1, Some(20));
2536 assert_eq!(locations[1].0, PathList::new(&["/tmp1"]));
2537 assert_eq!(locations[1].1, Some(10));
2538
2539 let locations = db.session_workspaces("session-id-2".to_owned()).unwrap();
2540 assert_eq!(locations.len(), 2);
2541 assert_eq!(locations[0].0, PathList::default());
2542 assert_eq!(locations[0].1, Some(50));
2543 assert_eq!(locations[0].2, Some(connection_id));
2544 assert_eq!(locations[1].0, PathList::new(&["/tmp3"]));
2545 assert_eq!(locations[1].1, Some(30));
2546
2547 let locations = db.session_workspaces("session-id-3".to_owned()).unwrap();
2548 assert_eq!(locations.len(), 1);
2549 assert_eq!(
2550 locations[0].0,
2551 PathList::new(&["/tmp6c", "/tmp6b", "/tmp6a"]),
2552 );
2553 assert_eq!(locations[0].1, Some(60));
2554 }
2555
2556 fn default_workspace<P: AsRef<Path>>(
2557 paths: &[P],
2558 center_group: &SerializedPaneGroup,
2559 ) -> SerializedWorkspace {
2560 SerializedWorkspace {
2561 id: WorkspaceId(4),
2562 paths: PathList::new(paths),
2563 location: SerializedWorkspaceLocation::Local,
2564 center_group: center_group.clone(),
2565 window_bounds: Default::default(),
2566 display: Default::default(),
2567 docks: Default::default(),
2568 breakpoints: Default::default(),
2569 centered_layout: false,
2570 session_id: None,
2571 window_id: None,
2572 user_toolchains: Default::default(),
2573 }
2574 }
2575
2576 #[gpui::test]
2577 async fn test_last_session_workspace_locations() {
2578 let dir1 = tempfile::TempDir::with_prefix("dir1").unwrap();
2579 let dir2 = tempfile::TempDir::with_prefix("dir2").unwrap();
2580 let dir3 = tempfile::TempDir::with_prefix("dir3").unwrap();
2581 let dir4 = tempfile::TempDir::with_prefix("dir4").unwrap();
2582
2583 let db =
2584 WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces").await;
2585
2586 let workspaces = [
2587 (1, vec![dir1.path()], 9),
2588 (2, vec![dir2.path()], 5),
2589 (3, vec![dir3.path()], 8),
2590 (4, vec![dir4.path()], 2),
2591 (5, vec![dir1.path(), dir2.path(), dir3.path()], 3),
2592 (6, vec![dir4.path(), dir3.path(), dir2.path()], 4),
2593 ]
2594 .into_iter()
2595 .map(|(id, paths, window_id)| SerializedWorkspace {
2596 id: WorkspaceId(id),
2597 paths: PathList::new(paths.as_slice()),
2598 location: SerializedWorkspaceLocation::Local,
2599 center_group: Default::default(),
2600 window_bounds: Default::default(),
2601 display: Default::default(),
2602 docks: Default::default(),
2603 centered_layout: false,
2604 session_id: Some("one-session".to_owned()),
2605 breakpoints: Default::default(),
2606 window_id: Some(window_id),
2607 user_toolchains: Default::default(),
2608 })
2609 .collect::<Vec<_>>();
2610
2611 for workspace in workspaces.iter() {
2612 db.save_workspace(workspace.clone()).await;
2613 }
2614
2615 let stack = Some(Vec::from([
2616 WindowId::from(2), // Top
2617 WindowId::from(8),
2618 WindowId::from(5),
2619 WindowId::from(9),
2620 WindowId::from(3),
2621 WindowId::from(4), // Bottom
2622 ]));
2623
2624 let locations = db
2625 .last_session_workspace_locations("one-session", stack)
2626 .unwrap();
2627 assert_eq!(
2628 locations,
2629 [
2630 (
2631 SerializedWorkspaceLocation::Local,
2632 PathList::new(&[dir4.path()])
2633 ),
2634 (
2635 SerializedWorkspaceLocation::Local,
2636 PathList::new(&[dir3.path()])
2637 ),
2638 (
2639 SerializedWorkspaceLocation::Local,
2640 PathList::new(&[dir2.path()])
2641 ),
2642 (
2643 SerializedWorkspaceLocation::Local,
2644 PathList::new(&[dir1.path()])
2645 ),
2646 (
2647 SerializedWorkspaceLocation::Local,
2648 PathList::new(&[dir1.path(), dir2.path(), dir3.path()])
2649 ),
2650 (
2651 SerializedWorkspaceLocation::Local,
2652 PathList::new(&[dir4.path(), dir3.path(), dir2.path()])
2653 ),
2654 ]
2655 );
2656 }
2657
2658 #[gpui::test]
2659 async fn test_last_session_workspace_locations_remote() {
2660 let db =
2661 WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces_remote")
2662 .await;
2663
2664 let remote_connections = [
2665 ("host-1", "my-user-1"),
2666 ("host-2", "my-user-2"),
2667 ("host-3", "my-user-3"),
2668 ("host-4", "my-user-4"),
2669 ]
2670 .into_iter()
2671 .map(|(host, user)| async {
2672 let options = RemoteConnectionOptions::Ssh(SshConnectionOptions {
2673 host: host.to_string(),
2674 username: Some(user.to_string()),
2675 ..Default::default()
2676 });
2677 db.get_or_create_remote_connection(options.clone())
2678 .await
2679 .unwrap();
2680 options
2681 })
2682 .collect::<Vec<_>>();
2683
2684 let remote_connections = futures::future::join_all(remote_connections).await;
2685
2686 let workspaces = [
2687 (1, remote_connections[0].clone(), 9),
2688 (2, remote_connections[1].clone(), 5),
2689 (3, remote_connections[2].clone(), 8),
2690 (4, remote_connections[3].clone(), 2),
2691 ]
2692 .into_iter()
2693 .map(|(id, remote_connection, window_id)| SerializedWorkspace {
2694 id: WorkspaceId(id),
2695 paths: PathList::default(),
2696 location: SerializedWorkspaceLocation::Remote(remote_connection),
2697 center_group: Default::default(),
2698 window_bounds: Default::default(),
2699 display: Default::default(),
2700 docks: Default::default(),
2701 centered_layout: false,
2702 session_id: Some("one-session".to_owned()),
2703 breakpoints: Default::default(),
2704 window_id: Some(window_id),
2705 user_toolchains: Default::default(),
2706 })
2707 .collect::<Vec<_>>();
2708
2709 for workspace in workspaces.iter() {
2710 db.save_workspace(workspace.clone()).await;
2711 }
2712
2713 let stack = Some(Vec::from([
2714 WindowId::from(2), // Top
2715 WindowId::from(8),
2716 WindowId::from(5),
2717 WindowId::from(9), // Bottom
2718 ]));
2719
2720 let have = db
2721 .last_session_workspace_locations("one-session", stack)
2722 .unwrap();
2723 assert_eq!(have.len(), 4);
2724 assert_eq!(
2725 have[0],
2726 (
2727 SerializedWorkspaceLocation::Remote(remote_connections[3].clone()),
2728 PathList::default()
2729 )
2730 );
2731 assert_eq!(
2732 have[1],
2733 (
2734 SerializedWorkspaceLocation::Remote(remote_connections[2].clone()),
2735 PathList::default()
2736 )
2737 );
2738 assert_eq!(
2739 have[2],
2740 (
2741 SerializedWorkspaceLocation::Remote(remote_connections[1].clone()),
2742 PathList::default()
2743 )
2744 );
2745 assert_eq!(
2746 have[3],
2747 (
2748 SerializedWorkspaceLocation::Remote(remote_connections[0].clone()),
2749 PathList::default()
2750 )
2751 );
2752 }
2753
2754 #[gpui::test]
2755 async fn test_get_or_create_ssh_project() {
2756 let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project").await;
2757
2758 let host = "example.com".to_string();
2759 let port = Some(22_u16);
2760 let user = Some("user".to_string());
2761
2762 let connection_id = db
2763 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2764 host: host.clone(),
2765 port,
2766 username: user.clone(),
2767 ..Default::default()
2768 }))
2769 .await
2770 .unwrap();
2771
2772 // Test that calling the function again with the same parameters returns the same project
2773 let same_connection = db
2774 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2775 host: host.clone(),
2776 port,
2777 username: user.clone(),
2778 ..Default::default()
2779 }))
2780 .await
2781 .unwrap();
2782
2783 assert_eq!(connection_id, same_connection);
2784
2785 // Test with different parameters
2786 let host2 = "otherexample.com".to_string();
2787 let port2 = None;
2788 let user2 = Some("otheruser".to_string());
2789
2790 let different_connection = db
2791 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2792 host: host2.clone(),
2793 port: port2,
2794 username: user2.clone(),
2795 ..Default::default()
2796 }))
2797 .await
2798 .unwrap();
2799
2800 assert_ne!(connection_id, different_connection);
2801 }
2802
2803 #[gpui::test]
2804 async fn test_get_or_create_ssh_project_with_null_user() {
2805 let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project_with_null_user").await;
2806
2807 let (host, port, user) = ("example.com".to_string(), None, None);
2808
2809 let connection_id = db
2810 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2811 host: host.clone(),
2812 port,
2813 username: None,
2814 ..Default::default()
2815 }))
2816 .await
2817 .unwrap();
2818
2819 let same_connection_id = db
2820 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2821 host: host.clone(),
2822 port,
2823 username: user.clone(),
2824 ..Default::default()
2825 }))
2826 .await
2827 .unwrap();
2828
2829 assert_eq!(connection_id, same_connection_id);
2830 }
2831
2832 #[gpui::test]
2833 async fn test_get_remote_connections() {
2834 let db = WorkspaceDb::open_test_db("test_get_remote_connections").await;
2835
2836 let connections = [
2837 ("example.com".to_string(), None, None),
2838 (
2839 "anotherexample.com".to_string(),
2840 Some(123_u16),
2841 Some("user2".to_string()),
2842 ),
2843 ("yetanother.com".to_string(), Some(345_u16), None),
2844 ];
2845
2846 let mut ids = Vec::new();
2847 for (host, port, user) in connections.iter() {
2848 ids.push(
2849 db.get_or_create_remote_connection(RemoteConnectionOptions::Ssh(
2850 SshConnectionOptions {
2851 host: host.clone(),
2852 port: *port,
2853 username: user.clone(),
2854 ..Default::default()
2855 },
2856 ))
2857 .await
2858 .unwrap(),
2859 );
2860 }
2861
2862 let stored_connections = db.remote_connections().unwrap();
2863 assert_eq!(
2864 stored_connections,
2865 [
2866 (
2867 ids[0],
2868 RemoteConnectionOptions::Ssh(SshConnectionOptions {
2869 host: "example.com".into(),
2870 port: None,
2871 username: None,
2872 ..Default::default()
2873 }),
2874 ),
2875 (
2876 ids[1],
2877 RemoteConnectionOptions::Ssh(SshConnectionOptions {
2878 host: "anotherexample.com".into(),
2879 port: Some(123),
2880 username: Some("user2".into()),
2881 ..Default::default()
2882 }),
2883 ),
2884 (
2885 ids[2],
2886 RemoteConnectionOptions::Ssh(SshConnectionOptions {
2887 host: "yetanother.com".into(),
2888 port: Some(345),
2889 username: None,
2890 ..Default::default()
2891 }),
2892 ),
2893 ]
2894 .into_iter()
2895 .collect::<HashMap<_, _>>(),
2896 );
2897 }
2898
2899 #[gpui::test]
2900 async fn test_simple_split() {
2901 zlog::init_test();
2902
2903 let db = WorkspaceDb::open_test_db("simple_split").await;
2904
2905 // -----------------
2906 // | 1,2 | 5,6 |
2907 // | - - - | |
2908 // | 3,4 | |
2909 // -----------------
2910 let center_pane = group(
2911 Axis::Horizontal,
2912 vec![
2913 group(
2914 Axis::Vertical,
2915 vec![
2916 SerializedPaneGroup::Pane(SerializedPane::new(
2917 vec![
2918 SerializedItem::new("Terminal", 1, false, false),
2919 SerializedItem::new("Terminal", 2, true, false),
2920 ],
2921 false,
2922 0,
2923 )),
2924 SerializedPaneGroup::Pane(SerializedPane::new(
2925 vec![
2926 SerializedItem::new("Terminal", 4, false, false),
2927 SerializedItem::new("Terminal", 3, true, false),
2928 ],
2929 true,
2930 0,
2931 )),
2932 ],
2933 ),
2934 SerializedPaneGroup::Pane(SerializedPane::new(
2935 vec![
2936 SerializedItem::new("Terminal", 5, true, false),
2937 SerializedItem::new("Terminal", 6, false, false),
2938 ],
2939 false,
2940 0,
2941 )),
2942 ],
2943 );
2944
2945 let workspace = default_workspace(&["/tmp"], ¢er_pane);
2946
2947 db.save_workspace(workspace.clone()).await;
2948
2949 let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
2950
2951 assert_eq!(workspace.center_group, new_workspace.center_group);
2952 }
2953
2954 #[gpui::test]
2955 async fn test_cleanup_panes() {
2956 zlog::init_test();
2957
2958 let db = WorkspaceDb::open_test_db("test_cleanup_panes").await;
2959
2960 let center_pane = group(
2961 Axis::Horizontal,
2962 vec![
2963 group(
2964 Axis::Vertical,
2965 vec![
2966 SerializedPaneGroup::Pane(SerializedPane::new(
2967 vec![
2968 SerializedItem::new("Terminal", 1, false, false),
2969 SerializedItem::new("Terminal", 2, true, false),
2970 ],
2971 false,
2972 0,
2973 )),
2974 SerializedPaneGroup::Pane(SerializedPane::new(
2975 vec![
2976 SerializedItem::new("Terminal", 4, false, false),
2977 SerializedItem::new("Terminal", 3, true, false),
2978 ],
2979 true,
2980 0,
2981 )),
2982 ],
2983 ),
2984 SerializedPaneGroup::Pane(SerializedPane::new(
2985 vec![
2986 SerializedItem::new("Terminal", 5, false, false),
2987 SerializedItem::new("Terminal", 6, true, false),
2988 ],
2989 false,
2990 0,
2991 )),
2992 ],
2993 );
2994
2995 let id = &["/tmp"];
2996
2997 let mut workspace = default_workspace(id, ¢er_pane);
2998
2999 db.save_workspace(workspace.clone()).await;
3000
3001 workspace.center_group = group(
3002 Axis::Vertical,
3003 vec![
3004 SerializedPaneGroup::Pane(SerializedPane::new(
3005 vec![
3006 SerializedItem::new("Terminal", 1, false, false),
3007 SerializedItem::new("Terminal", 2, true, false),
3008 ],
3009 false,
3010 0,
3011 )),
3012 SerializedPaneGroup::Pane(SerializedPane::new(
3013 vec![
3014 SerializedItem::new("Terminal", 4, true, false),
3015 SerializedItem::new("Terminal", 3, false, false),
3016 ],
3017 true,
3018 0,
3019 )),
3020 ],
3021 );
3022
3023 db.save_workspace(workspace.clone()).await;
3024
3025 let new_workspace = db.workspace_for_roots(id).unwrap();
3026
3027 assert_eq!(workspace.center_group, new_workspace.center_group);
3028 }
3029}