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