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(crate) async fn toolchains(
1660 &self,
1661 workspace_id: WorkspaceId,
1662 ) -> Result<Vec<(Toolchain, WorktreeId, Arc<RelPath>)>> {
1663 self.write(move |this| {
1664 let mut select = this
1665 .select_bound(sql!(
1666 SELECT
1667 name, path, worktree_id, relative_worktree_path, language_name, raw_json
1668 FROM toolchains
1669 WHERE workspace_id = ?
1670 ))
1671 .context("select toolchains")?;
1672
1673 let toolchain: Vec<(String, String, u64, String, String, String)> =
1674 select(workspace_id)?;
1675
1676 Ok(toolchain
1677 .into_iter()
1678 .filter_map(
1679 |(name, path, worktree_id, relative_worktree_path, language, json)| {
1680 Some((
1681 Toolchain {
1682 name: name.into(),
1683 path: path.into(),
1684 language_name: LanguageName::new(&language),
1685 as_json: serde_json::Value::from_str(&json).ok()?,
1686 },
1687 WorktreeId::from_proto(worktree_id),
1688 RelPath::from_proto(&relative_worktree_path).log_err()?,
1689 ))
1690 },
1691 )
1692 .collect())
1693 })
1694 .await
1695 }
1696
1697 pub async fn set_toolchain(
1698 &self,
1699 workspace_id: WorkspaceId,
1700 worktree_id: WorktreeId,
1701 relative_worktree_path: Arc<RelPath>,
1702 toolchain: Toolchain,
1703 ) -> Result<()> {
1704 log::debug!(
1705 "Setting toolchain for workspace, worktree: {worktree_id:?}, relative path: {relative_worktree_path:?}, toolchain: {}",
1706 toolchain.name
1707 );
1708 self.write(move |conn| {
1709 let mut insert = conn
1710 .exec_bound(sql!(
1711 INSERT INTO toolchains(workspace_id, worktree_id, relative_worktree_path, language_name, name, path, raw_json) VALUES (?, ?, ?, ?, ?, ?, ?)
1712 ON CONFLICT DO
1713 UPDATE SET
1714 name = ?5,
1715 path = ?6,
1716 raw_json = ?7
1717 ))
1718 .context("Preparing insertion")?;
1719
1720 insert((
1721 workspace_id,
1722 worktree_id.to_usize(),
1723 relative_worktree_path.as_unix_str(),
1724 toolchain.language_name.as_ref(),
1725 toolchain.name.as_ref(),
1726 toolchain.path.as_ref(),
1727 toolchain.as_json.to_string(),
1728 ))?;
1729
1730 Ok(())
1731 }).await
1732 }
1733}
1734
1735pub fn delete_unloaded_items(
1736 alive_items: Vec<ItemId>,
1737 workspace_id: WorkspaceId,
1738 table: &'static str,
1739 db: &ThreadSafeConnection,
1740 cx: &mut App,
1741) -> Task<Result<()>> {
1742 let db = db.clone();
1743 cx.spawn(async move |_| {
1744 let placeholders = alive_items
1745 .iter()
1746 .map(|_| "?")
1747 .collect::<Vec<&str>>()
1748 .join(", ");
1749
1750 let query = format!(
1751 "DELETE FROM {table} WHERE workspace_id = ? AND item_id NOT IN ({placeholders})"
1752 );
1753
1754 db.write(move |conn| {
1755 let mut statement = Statement::prepare(conn, query)?;
1756 let mut next_index = statement.bind(&workspace_id, 1)?;
1757 for id in alive_items {
1758 next_index = statement.bind(&id, next_index)?;
1759 }
1760 statement.exec()
1761 })
1762 .await
1763 })
1764}
1765
1766#[cfg(test)]
1767mod tests {
1768 use super::*;
1769 use crate::persistence::model::{
1770 SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
1771 };
1772 use gpui;
1773 use pretty_assertions::assert_eq;
1774 use remote::SshConnectionOptions;
1775 use std::{thread, time::Duration};
1776
1777 #[gpui::test]
1778 async fn test_breakpoints() {
1779 zlog::init_test();
1780
1781 let db = WorkspaceDb::open_test_db("test_breakpoints").await;
1782 let id = db.next_id().await.unwrap();
1783
1784 let path = Path::new("/tmp/test.rs");
1785
1786 let breakpoint = Breakpoint {
1787 position: 123,
1788 message: None,
1789 state: BreakpointState::Enabled,
1790 condition: None,
1791 hit_condition: None,
1792 };
1793
1794 let log_breakpoint = Breakpoint {
1795 position: 456,
1796 message: Some("Test log message".into()),
1797 state: BreakpointState::Enabled,
1798 condition: None,
1799 hit_condition: None,
1800 };
1801
1802 let disable_breakpoint = Breakpoint {
1803 position: 578,
1804 message: None,
1805 state: BreakpointState::Disabled,
1806 condition: None,
1807 hit_condition: None,
1808 };
1809
1810 let condition_breakpoint = Breakpoint {
1811 position: 789,
1812 message: None,
1813 state: BreakpointState::Enabled,
1814 condition: Some("x > 5".into()),
1815 hit_condition: None,
1816 };
1817
1818 let hit_condition_breakpoint = Breakpoint {
1819 position: 999,
1820 message: None,
1821 state: BreakpointState::Enabled,
1822 condition: None,
1823 hit_condition: Some(">= 3".into()),
1824 };
1825
1826 let workspace = SerializedWorkspace {
1827 id,
1828 paths: PathList::new(&["/tmp"]),
1829 location: SerializedWorkspaceLocation::Local,
1830 center_group: Default::default(),
1831 window_bounds: Default::default(),
1832 display: Default::default(),
1833 docks: Default::default(),
1834 centered_layout: false,
1835 breakpoints: {
1836 let mut map = collections::BTreeMap::default();
1837 map.insert(
1838 Arc::from(path),
1839 vec![
1840 SourceBreakpoint {
1841 row: breakpoint.position,
1842 path: Arc::from(path),
1843 message: breakpoint.message.clone(),
1844 state: breakpoint.state,
1845 condition: breakpoint.condition.clone(),
1846 hit_condition: breakpoint.hit_condition.clone(),
1847 },
1848 SourceBreakpoint {
1849 row: log_breakpoint.position,
1850 path: Arc::from(path),
1851 message: log_breakpoint.message.clone(),
1852 state: log_breakpoint.state,
1853 condition: log_breakpoint.condition.clone(),
1854 hit_condition: log_breakpoint.hit_condition.clone(),
1855 },
1856 SourceBreakpoint {
1857 row: disable_breakpoint.position,
1858 path: Arc::from(path),
1859 message: disable_breakpoint.message.clone(),
1860 state: disable_breakpoint.state,
1861 condition: disable_breakpoint.condition.clone(),
1862 hit_condition: disable_breakpoint.hit_condition.clone(),
1863 },
1864 SourceBreakpoint {
1865 row: condition_breakpoint.position,
1866 path: Arc::from(path),
1867 message: condition_breakpoint.message.clone(),
1868 state: condition_breakpoint.state,
1869 condition: condition_breakpoint.condition.clone(),
1870 hit_condition: condition_breakpoint.hit_condition.clone(),
1871 },
1872 SourceBreakpoint {
1873 row: hit_condition_breakpoint.position,
1874 path: Arc::from(path),
1875 message: hit_condition_breakpoint.message.clone(),
1876 state: hit_condition_breakpoint.state,
1877 condition: hit_condition_breakpoint.condition.clone(),
1878 hit_condition: hit_condition_breakpoint.hit_condition.clone(),
1879 },
1880 ],
1881 );
1882 map
1883 },
1884 session_id: None,
1885 window_id: None,
1886 user_toolchains: Default::default(),
1887 };
1888
1889 db.save_workspace(workspace.clone()).await;
1890
1891 let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
1892 let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(path)).unwrap();
1893
1894 assert_eq!(loaded_breakpoints.len(), 5);
1895
1896 // normal breakpoint
1897 assert_eq!(loaded_breakpoints[0].row, breakpoint.position);
1898 assert_eq!(loaded_breakpoints[0].message, breakpoint.message);
1899 assert_eq!(loaded_breakpoints[0].condition, breakpoint.condition);
1900 assert_eq!(
1901 loaded_breakpoints[0].hit_condition,
1902 breakpoint.hit_condition
1903 );
1904 assert_eq!(loaded_breakpoints[0].state, breakpoint.state);
1905 assert_eq!(loaded_breakpoints[0].path, Arc::from(path));
1906
1907 // enabled breakpoint
1908 assert_eq!(loaded_breakpoints[1].row, log_breakpoint.position);
1909 assert_eq!(loaded_breakpoints[1].message, log_breakpoint.message);
1910 assert_eq!(loaded_breakpoints[1].condition, log_breakpoint.condition);
1911 assert_eq!(
1912 loaded_breakpoints[1].hit_condition,
1913 log_breakpoint.hit_condition
1914 );
1915 assert_eq!(loaded_breakpoints[1].state, log_breakpoint.state);
1916 assert_eq!(loaded_breakpoints[1].path, Arc::from(path));
1917
1918 // disable breakpoint
1919 assert_eq!(loaded_breakpoints[2].row, disable_breakpoint.position);
1920 assert_eq!(loaded_breakpoints[2].message, disable_breakpoint.message);
1921 assert_eq!(
1922 loaded_breakpoints[2].condition,
1923 disable_breakpoint.condition
1924 );
1925 assert_eq!(
1926 loaded_breakpoints[2].hit_condition,
1927 disable_breakpoint.hit_condition
1928 );
1929 assert_eq!(loaded_breakpoints[2].state, disable_breakpoint.state);
1930 assert_eq!(loaded_breakpoints[2].path, Arc::from(path));
1931
1932 // condition breakpoint
1933 assert_eq!(loaded_breakpoints[3].row, condition_breakpoint.position);
1934 assert_eq!(loaded_breakpoints[3].message, condition_breakpoint.message);
1935 assert_eq!(
1936 loaded_breakpoints[3].condition,
1937 condition_breakpoint.condition
1938 );
1939 assert_eq!(
1940 loaded_breakpoints[3].hit_condition,
1941 condition_breakpoint.hit_condition
1942 );
1943 assert_eq!(loaded_breakpoints[3].state, condition_breakpoint.state);
1944 assert_eq!(loaded_breakpoints[3].path, Arc::from(path));
1945
1946 // hit condition breakpoint
1947 assert_eq!(loaded_breakpoints[4].row, hit_condition_breakpoint.position);
1948 assert_eq!(
1949 loaded_breakpoints[4].message,
1950 hit_condition_breakpoint.message
1951 );
1952 assert_eq!(
1953 loaded_breakpoints[4].condition,
1954 hit_condition_breakpoint.condition
1955 );
1956 assert_eq!(
1957 loaded_breakpoints[4].hit_condition,
1958 hit_condition_breakpoint.hit_condition
1959 );
1960 assert_eq!(loaded_breakpoints[4].state, hit_condition_breakpoint.state);
1961 assert_eq!(loaded_breakpoints[4].path, Arc::from(path));
1962 }
1963
1964 #[gpui::test]
1965 async fn test_remove_last_breakpoint() {
1966 zlog::init_test();
1967
1968 let db = WorkspaceDb::open_test_db("test_remove_last_breakpoint").await;
1969 let id = db.next_id().await.unwrap();
1970
1971 let singular_path = Path::new("/tmp/test_remove_last_breakpoint.rs");
1972
1973 let breakpoint_to_remove = Breakpoint {
1974 position: 100,
1975 message: None,
1976 state: BreakpointState::Enabled,
1977 condition: None,
1978 hit_condition: None,
1979 };
1980
1981 let workspace = SerializedWorkspace {
1982 id,
1983 paths: PathList::new(&["/tmp"]),
1984 location: SerializedWorkspaceLocation::Local,
1985 center_group: Default::default(),
1986 window_bounds: Default::default(),
1987 display: Default::default(),
1988 docks: Default::default(),
1989 centered_layout: false,
1990 breakpoints: {
1991 let mut map = collections::BTreeMap::default();
1992 map.insert(
1993 Arc::from(singular_path),
1994 vec![SourceBreakpoint {
1995 row: breakpoint_to_remove.position,
1996 path: Arc::from(singular_path),
1997 message: None,
1998 state: BreakpointState::Enabled,
1999 condition: None,
2000 hit_condition: None,
2001 }],
2002 );
2003 map
2004 },
2005 session_id: None,
2006 window_id: None,
2007 user_toolchains: Default::default(),
2008 };
2009
2010 db.save_workspace(workspace.clone()).await;
2011
2012 let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
2013 let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(singular_path)).unwrap();
2014
2015 assert_eq!(loaded_breakpoints.len(), 1);
2016 assert_eq!(loaded_breakpoints[0].row, breakpoint_to_remove.position);
2017 assert_eq!(loaded_breakpoints[0].message, breakpoint_to_remove.message);
2018 assert_eq!(
2019 loaded_breakpoints[0].condition,
2020 breakpoint_to_remove.condition
2021 );
2022 assert_eq!(
2023 loaded_breakpoints[0].hit_condition,
2024 breakpoint_to_remove.hit_condition
2025 );
2026 assert_eq!(loaded_breakpoints[0].state, breakpoint_to_remove.state);
2027 assert_eq!(loaded_breakpoints[0].path, Arc::from(singular_path));
2028
2029 let workspace_without_breakpoint = SerializedWorkspace {
2030 id,
2031 paths: PathList::new(&["/tmp"]),
2032 location: SerializedWorkspaceLocation::Local,
2033 center_group: Default::default(),
2034 window_bounds: Default::default(),
2035 display: Default::default(),
2036 docks: Default::default(),
2037 centered_layout: false,
2038 breakpoints: collections::BTreeMap::default(),
2039 session_id: None,
2040 window_id: None,
2041 user_toolchains: Default::default(),
2042 };
2043
2044 db.save_workspace(workspace_without_breakpoint.clone())
2045 .await;
2046
2047 let loaded_after_remove = db.workspace_for_roots(&["/tmp"]).unwrap();
2048 let empty_breakpoints = loaded_after_remove
2049 .breakpoints
2050 .get(&Arc::from(singular_path));
2051
2052 assert!(empty_breakpoints.is_none());
2053 }
2054
2055 #[gpui::test]
2056 async fn test_next_id_stability() {
2057 zlog::init_test();
2058
2059 let db = WorkspaceDb::open_test_db("test_next_id_stability").await;
2060
2061 db.write(|conn| {
2062 conn.migrate(
2063 "test_table",
2064 &[sql!(
2065 CREATE TABLE test_table(
2066 text TEXT,
2067 workspace_id INTEGER,
2068 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
2069 ON DELETE CASCADE
2070 ) STRICT;
2071 )],
2072 |_, _, _| false,
2073 )
2074 .unwrap();
2075 })
2076 .await;
2077
2078 let id = db.next_id().await.unwrap();
2079 // Assert the empty row got inserted
2080 assert_eq!(
2081 Some(id),
2082 db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
2083 SELECT workspace_id FROM workspaces WHERE workspace_id = ?
2084 ))
2085 .unwrap()(id)
2086 .unwrap()
2087 );
2088
2089 db.write(move |conn| {
2090 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2091 .unwrap()(("test-text-1", id))
2092 .unwrap()
2093 })
2094 .await;
2095
2096 let test_text_1 = db
2097 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2098 .unwrap()(1)
2099 .unwrap()
2100 .unwrap();
2101 assert_eq!(test_text_1, "test-text-1");
2102 }
2103
2104 #[gpui::test]
2105 async fn test_workspace_id_stability() {
2106 zlog::init_test();
2107
2108 let db = WorkspaceDb::open_test_db("test_workspace_id_stability").await;
2109
2110 db.write(|conn| {
2111 conn.migrate(
2112 "test_table",
2113 &[sql!(
2114 CREATE TABLE test_table(
2115 text TEXT,
2116 workspace_id INTEGER,
2117 FOREIGN KEY(workspace_id)
2118 REFERENCES workspaces(workspace_id)
2119 ON DELETE CASCADE
2120 ) STRICT;)],
2121 |_, _, _| false,
2122 )
2123 })
2124 .await
2125 .unwrap();
2126
2127 let mut workspace_1 = SerializedWorkspace {
2128 id: WorkspaceId(1),
2129 paths: PathList::new(&["/tmp", "/tmp2"]),
2130 location: SerializedWorkspaceLocation::Local,
2131 center_group: Default::default(),
2132 window_bounds: Default::default(),
2133 display: Default::default(),
2134 docks: Default::default(),
2135 centered_layout: false,
2136 breakpoints: Default::default(),
2137 session_id: None,
2138 window_id: None,
2139 user_toolchains: Default::default(),
2140 };
2141
2142 let workspace_2 = SerializedWorkspace {
2143 id: WorkspaceId(2),
2144 paths: PathList::new(&["/tmp"]),
2145 location: SerializedWorkspaceLocation::Local,
2146 center_group: Default::default(),
2147 window_bounds: Default::default(),
2148 display: Default::default(),
2149 docks: Default::default(),
2150 centered_layout: false,
2151 breakpoints: Default::default(),
2152 session_id: None,
2153 window_id: None,
2154 user_toolchains: Default::default(),
2155 };
2156
2157 db.save_workspace(workspace_1.clone()).await;
2158
2159 db.write(|conn| {
2160 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2161 .unwrap()(("test-text-1", 1))
2162 .unwrap();
2163 })
2164 .await;
2165
2166 db.save_workspace(workspace_2.clone()).await;
2167
2168 db.write(|conn| {
2169 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2170 .unwrap()(("test-text-2", 2))
2171 .unwrap();
2172 })
2173 .await;
2174
2175 workspace_1.paths = PathList::new(&["/tmp", "/tmp3"]);
2176 db.save_workspace(workspace_1.clone()).await;
2177 db.save_workspace(workspace_1).await;
2178 db.save_workspace(workspace_2).await;
2179
2180 let test_text_2 = db
2181 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2182 .unwrap()(2)
2183 .unwrap()
2184 .unwrap();
2185 assert_eq!(test_text_2, "test-text-2");
2186
2187 let test_text_1 = db
2188 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2189 .unwrap()(1)
2190 .unwrap()
2191 .unwrap();
2192 assert_eq!(test_text_1, "test-text-1");
2193 }
2194
2195 fn group(axis: Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
2196 SerializedPaneGroup::Group {
2197 axis: SerializedAxis(axis),
2198 flexes: None,
2199 children,
2200 }
2201 }
2202
2203 #[gpui::test]
2204 async fn test_full_workspace_serialization() {
2205 zlog::init_test();
2206
2207 let db = WorkspaceDb::open_test_db("test_full_workspace_serialization").await;
2208
2209 // -----------------
2210 // | 1,2 | 5,6 |
2211 // | - - - | |
2212 // | 3,4 | |
2213 // -----------------
2214 let center_group = group(
2215 Axis::Horizontal,
2216 vec![
2217 group(
2218 Axis::Vertical,
2219 vec![
2220 SerializedPaneGroup::Pane(SerializedPane::new(
2221 vec![
2222 SerializedItem::new("Terminal", 5, false, false),
2223 SerializedItem::new("Terminal", 6, true, false),
2224 ],
2225 false,
2226 0,
2227 )),
2228 SerializedPaneGroup::Pane(SerializedPane::new(
2229 vec![
2230 SerializedItem::new("Terminal", 7, true, false),
2231 SerializedItem::new("Terminal", 8, false, false),
2232 ],
2233 false,
2234 0,
2235 )),
2236 ],
2237 ),
2238 SerializedPaneGroup::Pane(SerializedPane::new(
2239 vec![
2240 SerializedItem::new("Terminal", 9, false, false),
2241 SerializedItem::new("Terminal", 10, true, false),
2242 ],
2243 false,
2244 0,
2245 )),
2246 ],
2247 );
2248
2249 let workspace = SerializedWorkspace {
2250 id: WorkspaceId(5),
2251 paths: PathList::new(&["/tmp", "/tmp2"]),
2252 location: SerializedWorkspaceLocation::Local,
2253 center_group,
2254 window_bounds: Default::default(),
2255 breakpoints: Default::default(),
2256 display: Default::default(),
2257 docks: Default::default(),
2258 centered_layout: false,
2259 session_id: None,
2260 window_id: Some(999),
2261 user_toolchains: Default::default(),
2262 };
2263
2264 db.save_workspace(workspace.clone()).await;
2265
2266 let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
2267 assert_eq!(workspace, round_trip_workspace.unwrap());
2268
2269 // Test guaranteed duplicate IDs
2270 db.save_workspace(workspace.clone()).await;
2271 db.save_workspace(workspace.clone()).await;
2272
2273 let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
2274 assert_eq!(workspace, round_trip_workspace.unwrap());
2275 }
2276
2277 #[gpui::test]
2278 async fn test_workspace_assignment() {
2279 zlog::init_test();
2280
2281 let db = WorkspaceDb::open_test_db("test_basic_functionality").await;
2282
2283 let workspace_1 = SerializedWorkspace {
2284 id: WorkspaceId(1),
2285 paths: PathList::new(&["/tmp", "/tmp2"]),
2286 location: SerializedWorkspaceLocation::Local,
2287 center_group: Default::default(),
2288 window_bounds: Default::default(),
2289 breakpoints: Default::default(),
2290 display: Default::default(),
2291 docks: Default::default(),
2292 centered_layout: false,
2293 session_id: None,
2294 window_id: Some(1),
2295 user_toolchains: Default::default(),
2296 };
2297
2298 let mut workspace_2 = SerializedWorkspace {
2299 id: WorkspaceId(2),
2300 paths: PathList::new(&["/tmp"]),
2301 location: SerializedWorkspaceLocation::Local,
2302 center_group: Default::default(),
2303 window_bounds: Default::default(),
2304 display: Default::default(),
2305 docks: Default::default(),
2306 centered_layout: false,
2307 breakpoints: Default::default(),
2308 session_id: None,
2309 window_id: Some(2),
2310 user_toolchains: Default::default(),
2311 };
2312
2313 db.save_workspace(workspace_1.clone()).await;
2314 db.save_workspace(workspace_2.clone()).await;
2315
2316 // Test that paths are treated as a set
2317 assert_eq!(
2318 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2319 workspace_1
2320 );
2321 assert_eq!(
2322 db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
2323 workspace_1
2324 );
2325
2326 // Make sure that other keys work
2327 assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
2328 assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
2329
2330 // Test 'mutate' case of updating a pre-existing id
2331 workspace_2.paths = PathList::new(&["/tmp", "/tmp2"]);
2332
2333 db.save_workspace(workspace_2.clone()).await;
2334 assert_eq!(
2335 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2336 workspace_2
2337 );
2338
2339 // Test other mechanism for mutating
2340 let mut workspace_3 = SerializedWorkspace {
2341 id: WorkspaceId(3),
2342 paths: PathList::new(&["/tmp2", "/tmp"]),
2343 location: SerializedWorkspaceLocation::Local,
2344 center_group: Default::default(),
2345 window_bounds: Default::default(),
2346 breakpoints: Default::default(),
2347 display: Default::default(),
2348 docks: Default::default(),
2349 centered_layout: false,
2350 session_id: None,
2351 window_id: Some(3),
2352 user_toolchains: Default::default(),
2353 };
2354
2355 db.save_workspace(workspace_3.clone()).await;
2356 assert_eq!(
2357 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2358 workspace_3
2359 );
2360
2361 // Make sure that updating paths differently also works
2362 workspace_3.paths = PathList::new(&["/tmp3", "/tmp4", "/tmp2"]);
2363 db.save_workspace(workspace_3.clone()).await;
2364 assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
2365 assert_eq!(
2366 db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
2367 .unwrap(),
2368 workspace_3
2369 );
2370 }
2371
2372 #[gpui::test]
2373 async fn test_session_workspaces() {
2374 zlog::init_test();
2375
2376 let db = WorkspaceDb::open_test_db("test_serializing_workspaces_session_id").await;
2377
2378 let workspace_1 = SerializedWorkspace {
2379 id: WorkspaceId(1),
2380 paths: PathList::new(&["/tmp1"]),
2381 location: SerializedWorkspaceLocation::Local,
2382 center_group: Default::default(),
2383 window_bounds: Default::default(),
2384 display: Default::default(),
2385 docks: Default::default(),
2386 centered_layout: false,
2387 breakpoints: Default::default(),
2388 session_id: Some("session-id-1".to_owned()),
2389 window_id: Some(10),
2390 user_toolchains: Default::default(),
2391 };
2392
2393 let workspace_2 = SerializedWorkspace {
2394 id: WorkspaceId(2),
2395 paths: PathList::new(&["/tmp2"]),
2396 location: SerializedWorkspaceLocation::Local,
2397 center_group: Default::default(),
2398 window_bounds: Default::default(),
2399 display: Default::default(),
2400 docks: Default::default(),
2401 centered_layout: false,
2402 breakpoints: Default::default(),
2403 session_id: Some("session-id-1".to_owned()),
2404 window_id: Some(20),
2405 user_toolchains: Default::default(),
2406 };
2407
2408 let workspace_3 = SerializedWorkspace {
2409 id: WorkspaceId(3),
2410 paths: PathList::new(&["/tmp3"]),
2411 location: SerializedWorkspaceLocation::Local,
2412 center_group: Default::default(),
2413 window_bounds: Default::default(),
2414 display: Default::default(),
2415 docks: Default::default(),
2416 centered_layout: false,
2417 breakpoints: Default::default(),
2418 session_id: Some("session-id-2".to_owned()),
2419 window_id: Some(30),
2420 user_toolchains: Default::default(),
2421 };
2422
2423 let workspace_4 = SerializedWorkspace {
2424 id: WorkspaceId(4),
2425 paths: PathList::new(&["/tmp4"]),
2426 location: SerializedWorkspaceLocation::Local,
2427 center_group: Default::default(),
2428 window_bounds: Default::default(),
2429 display: Default::default(),
2430 docks: Default::default(),
2431 centered_layout: false,
2432 breakpoints: Default::default(),
2433 session_id: None,
2434 window_id: None,
2435 user_toolchains: Default::default(),
2436 };
2437
2438 let connection_id = db
2439 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2440 host: "my-host".to_string(),
2441 port: Some(1234),
2442 ..Default::default()
2443 }))
2444 .await
2445 .unwrap();
2446
2447 let workspace_5 = SerializedWorkspace {
2448 id: WorkspaceId(5),
2449 paths: PathList::default(),
2450 location: SerializedWorkspaceLocation::Remote(
2451 db.remote_connection(connection_id).unwrap(),
2452 ),
2453 center_group: Default::default(),
2454 window_bounds: Default::default(),
2455 display: Default::default(),
2456 docks: Default::default(),
2457 centered_layout: false,
2458 breakpoints: Default::default(),
2459 session_id: Some("session-id-2".to_owned()),
2460 window_id: Some(50),
2461 user_toolchains: Default::default(),
2462 };
2463
2464 let workspace_6 = SerializedWorkspace {
2465 id: WorkspaceId(6),
2466 paths: PathList::new(&["/tmp6c", "/tmp6b", "/tmp6a"]),
2467 location: SerializedWorkspaceLocation::Local,
2468 center_group: Default::default(),
2469 window_bounds: Default::default(),
2470 breakpoints: Default::default(),
2471 display: Default::default(),
2472 docks: Default::default(),
2473 centered_layout: false,
2474 session_id: Some("session-id-3".to_owned()),
2475 window_id: Some(60),
2476 user_toolchains: Default::default(),
2477 };
2478
2479 db.save_workspace(workspace_1.clone()).await;
2480 thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
2481 db.save_workspace(workspace_2.clone()).await;
2482 db.save_workspace(workspace_3.clone()).await;
2483 thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
2484 db.save_workspace(workspace_4.clone()).await;
2485 db.save_workspace(workspace_5.clone()).await;
2486 db.save_workspace(workspace_6.clone()).await;
2487
2488 let locations = db.session_workspaces("session-id-1".to_owned()).unwrap();
2489 assert_eq!(locations.len(), 2);
2490 assert_eq!(locations[0].0, PathList::new(&["/tmp2"]));
2491 assert_eq!(locations[0].1, Some(20));
2492 assert_eq!(locations[1].0, PathList::new(&["/tmp1"]));
2493 assert_eq!(locations[1].1, Some(10));
2494
2495 let locations = db.session_workspaces("session-id-2".to_owned()).unwrap();
2496 assert_eq!(locations.len(), 2);
2497 assert_eq!(locations[0].0, PathList::default());
2498 assert_eq!(locations[0].1, Some(50));
2499 assert_eq!(locations[0].2, Some(connection_id));
2500 assert_eq!(locations[1].0, PathList::new(&["/tmp3"]));
2501 assert_eq!(locations[1].1, Some(30));
2502
2503 let locations = db.session_workspaces("session-id-3".to_owned()).unwrap();
2504 assert_eq!(locations.len(), 1);
2505 assert_eq!(
2506 locations[0].0,
2507 PathList::new(&["/tmp6c", "/tmp6b", "/tmp6a"]),
2508 );
2509 assert_eq!(locations[0].1, Some(60));
2510 }
2511
2512 fn default_workspace<P: AsRef<Path>>(
2513 paths: &[P],
2514 center_group: &SerializedPaneGroup,
2515 ) -> SerializedWorkspace {
2516 SerializedWorkspace {
2517 id: WorkspaceId(4),
2518 paths: PathList::new(paths),
2519 location: SerializedWorkspaceLocation::Local,
2520 center_group: center_group.clone(),
2521 window_bounds: Default::default(),
2522 display: Default::default(),
2523 docks: Default::default(),
2524 breakpoints: Default::default(),
2525 centered_layout: false,
2526 session_id: None,
2527 window_id: None,
2528 user_toolchains: Default::default(),
2529 }
2530 }
2531
2532 #[gpui::test]
2533 async fn test_last_session_workspace_locations() {
2534 let dir1 = tempfile::TempDir::with_prefix("dir1").unwrap();
2535 let dir2 = tempfile::TempDir::with_prefix("dir2").unwrap();
2536 let dir3 = tempfile::TempDir::with_prefix("dir3").unwrap();
2537 let dir4 = tempfile::TempDir::with_prefix("dir4").unwrap();
2538
2539 let db =
2540 WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces").await;
2541
2542 let workspaces = [
2543 (1, vec![dir1.path()], 9),
2544 (2, vec![dir2.path()], 5),
2545 (3, vec![dir3.path()], 8),
2546 (4, vec![dir4.path()], 2),
2547 (5, vec![dir1.path(), dir2.path(), dir3.path()], 3),
2548 (6, vec![dir4.path(), dir3.path(), dir2.path()], 4),
2549 ]
2550 .into_iter()
2551 .map(|(id, paths, window_id)| SerializedWorkspace {
2552 id: WorkspaceId(id),
2553 paths: PathList::new(paths.as_slice()),
2554 location: SerializedWorkspaceLocation::Local,
2555 center_group: Default::default(),
2556 window_bounds: Default::default(),
2557 display: Default::default(),
2558 docks: Default::default(),
2559 centered_layout: false,
2560 session_id: Some("one-session".to_owned()),
2561 breakpoints: Default::default(),
2562 window_id: Some(window_id),
2563 user_toolchains: Default::default(),
2564 })
2565 .collect::<Vec<_>>();
2566
2567 for workspace in workspaces.iter() {
2568 db.save_workspace(workspace.clone()).await;
2569 }
2570
2571 let stack = Some(Vec::from([
2572 WindowId::from(2), // Top
2573 WindowId::from(8),
2574 WindowId::from(5),
2575 WindowId::from(9),
2576 WindowId::from(3),
2577 WindowId::from(4), // Bottom
2578 ]));
2579
2580 let locations = db
2581 .last_session_workspace_locations("one-session", stack)
2582 .unwrap();
2583 assert_eq!(
2584 locations,
2585 [
2586 (
2587 SerializedWorkspaceLocation::Local,
2588 PathList::new(&[dir4.path()])
2589 ),
2590 (
2591 SerializedWorkspaceLocation::Local,
2592 PathList::new(&[dir3.path()])
2593 ),
2594 (
2595 SerializedWorkspaceLocation::Local,
2596 PathList::new(&[dir2.path()])
2597 ),
2598 (
2599 SerializedWorkspaceLocation::Local,
2600 PathList::new(&[dir1.path()])
2601 ),
2602 (
2603 SerializedWorkspaceLocation::Local,
2604 PathList::new(&[dir1.path(), dir2.path(), dir3.path()])
2605 ),
2606 (
2607 SerializedWorkspaceLocation::Local,
2608 PathList::new(&[dir4.path(), dir3.path(), dir2.path()])
2609 ),
2610 ]
2611 );
2612 }
2613
2614 #[gpui::test]
2615 async fn test_last_session_workspace_locations_remote() {
2616 let db =
2617 WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces_remote")
2618 .await;
2619
2620 let remote_connections = [
2621 ("host-1", "my-user-1"),
2622 ("host-2", "my-user-2"),
2623 ("host-3", "my-user-3"),
2624 ("host-4", "my-user-4"),
2625 ]
2626 .into_iter()
2627 .map(|(host, user)| async {
2628 let options = RemoteConnectionOptions::Ssh(SshConnectionOptions {
2629 host: host.to_string(),
2630 username: Some(user.to_string()),
2631 ..Default::default()
2632 });
2633 db.get_or_create_remote_connection(options.clone())
2634 .await
2635 .unwrap();
2636 options
2637 })
2638 .collect::<Vec<_>>();
2639
2640 let remote_connections = futures::future::join_all(remote_connections).await;
2641
2642 let workspaces = [
2643 (1, remote_connections[0].clone(), 9),
2644 (2, remote_connections[1].clone(), 5),
2645 (3, remote_connections[2].clone(), 8),
2646 (4, remote_connections[3].clone(), 2),
2647 ]
2648 .into_iter()
2649 .map(|(id, remote_connection, window_id)| SerializedWorkspace {
2650 id: WorkspaceId(id),
2651 paths: PathList::default(),
2652 location: SerializedWorkspaceLocation::Remote(remote_connection),
2653 center_group: Default::default(),
2654 window_bounds: Default::default(),
2655 display: Default::default(),
2656 docks: Default::default(),
2657 centered_layout: false,
2658 session_id: Some("one-session".to_owned()),
2659 breakpoints: Default::default(),
2660 window_id: Some(window_id),
2661 user_toolchains: Default::default(),
2662 })
2663 .collect::<Vec<_>>();
2664
2665 for workspace in workspaces.iter() {
2666 db.save_workspace(workspace.clone()).await;
2667 }
2668
2669 let stack = Some(Vec::from([
2670 WindowId::from(2), // Top
2671 WindowId::from(8),
2672 WindowId::from(5),
2673 WindowId::from(9), // Bottom
2674 ]));
2675
2676 let have = db
2677 .last_session_workspace_locations("one-session", stack)
2678 .unwrap();
2679 assert_eq!(have.len(), 4);
2680 assert_eq!(
2681 have[0],
2682 (
2683 SerializedWorkspaceLocation::Remote(remote_connections[3].clone()),
2684 PathList::default()
2685 )
2686 );
2687 assert_eq!(
2688 have[1],
2689 (
2690 SerializedWorkspaceLocation::Remote(remote_connections[2].clone()),
2691 PathList::default()
2692 )
2693 );
2694 assert_eq!(
2695 have[2],
2696 (
2697 SerializedWorkspaceLocation::Remote(remote_connections[1].clone()),
2698 PathList::default()
2699 )
2700 );
2701 assert_eq!(
2702 have[3],
2703 (
2704 SerializedWorkspaceLocation::Remote(remote_connections[0].clone()),
2705 PathList::default()
2706 )
2707 );
2708 }
2709
2710 #[gpui::test]
2711 async fn test_get_or_create_ssh_project() {
2712 let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project").await;
2713
2714 let host = "example.com".to_string();
2715 let port = Some(22_u16);
2716 let user = Some("user".to_string());
2717
2718 let connection_id = db
2719 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2720 host: host.clone(),
2721 port,
2722 username: user.clone(),
2723 ..Default::default()
2724 }))
2725 .await
2726 .unwrap();
2727
2728 // Test that calling the function again with the same parameters returns the same project
2729 let same_connection = db
2730 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2731 host: host.clone(),
2732 port,
2733 username: user.clone(),
2734 ..Default::default()
2735 }))
2736 .await
2737 .unwrap();
2738
2739 assert_eq!(connection_id, same_connection);
2740
2741 // Test with different parameters
2742 let host2 = "otherexample.com".to_string();
2743 let port2 = None;
2744 let user2 = Some("otheruser".to_string());
2745
2746 let different_connection = db
2747 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2748 host: host2.clone(),
2749 port: port2,
2750 username: user2.clone(),
2751 ..Default::default()
2752 }))
2753 .await
2754 .unwrap();
2755
2756 assert_ne!(connection_id, different_connection);
2757 }
2758
2759 #[gpui::test]
2760 async fn test_get_or_create_ssh_project_with_null_user() {
2761 let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project_with_null_user").await;
2762
2763 let (host, port, user) = ("example.com".to_string(), None, None);
2764
2765 let connection_id = db
2766 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2767 host: host.clone(),
2768 port,
2769 username: None,
2770 ..Default::default()
2771 }))
2772 .await
2773 .unwrap();
2774
2775 let same_connection_id = db
2776 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2777 host: host.clone(),
2778 port,
2779 username: user.clone(),
2780 ..Default::default()
2781 }))
2782 .await
2783 .unwrap();
2784
2785 assert_eq!(connection_id, same_connection_id);
2786 }
2787
2788 #[gpui::test]
2789 async fn test_get_remote_connections() {
2790 let db = WorkspaceDb::open_test_db("test_get_remote_connections").await;
2791
2792 let connections = [
2793 ("example.com".to_string(), None, None),
2794 (
2795 "anotherexample.com".to_string(),
2796 Some(123_u16),
2797 Some("user2".to_string()),
2798 ),
2799 ("yetanother.com".to_string(), Some(345_u16), None),
2800 ];
2801
2802 let mut ids = Vec::new();
2803 for (host, port, user) in connections.iter() {
2804 ids.push(
2805 db.get_or_create_remote_connection(RemoteConnectionOptions::Ssh(
2806 SshConnectionOptions {
2807 host: host.clone(),
2808 port: *port,
2809 username: user.clone(),
2810 ..Default::default()
2811 },
2812 ))
2813 .await
2814 .unwrap(),
2815 );
2816 }
2817
2818 let stored_connections = db.remote_connections().unwrap();
2819 assert_eq!(
2820 stored_connections,
2821 [
2822 (
2823 ids[0],
2824 RemoteConnectionOptions::Ssh(SshConnectionOptions {
2825 host: "example.com".into(),
2826 port: None,
2827 username: None,
2828 ..Default::default()
2829 }),
2830 ),
2831 (
2832 ids[1],
2833 RemoteConnectionOptions::Ssh(SshConnectionOptions {
2834 host: "anotherexample.com".into(),
2835 port: Some(123),
2836 username: Some("user2".into()),
2837 ..Default::default()
2838 }),
2839 ),
2840 (
2841 ids[2],
2842 RemoteConnectionOptions::Ssh(SshConnectionOptions {
2843 host: "yetanother.com".into(),
2844 port: Some(345),
2845 username: None,
2846 ..Default::default()
2847 }),
2848 ),
2849 ]
2850 .into_iter()
2851 .collect::<HashMap<_, _>>(),
2852 );
2853 }
2854
2855 #[gpui::test]
2856 async fn test_simple_split() {
2857 zlog::init_test();
2858
2859 let db = WorkspaceDb::open_test_db("simple_split").await;
2860
2861 // -----------------
2862 // | 1,2 | 5,6 |
2863 // | - - - | |
2864 // | 3,4 | |
2865 // -----------------
2866 let center_pane = group(
2867 Axis::Horizontal,
2868 vec![
2869 group(
2870 Axis::Vertical,
2871 vec![
2872 SerializedPaneGroup::Pane(SerializedPane::new(
2873 vec![
2874 SerializedItem::new("Terminal", 1, false, false),
2875 SerializedItem::new("Terminal", 2, true, false),
2876 ],
2877 false,
2878 0,
2879 )),
2880 SerializedPaneGroup::Pane(SerializedPane::new(
2881 vec![
2882 SerializedItem::new("Terminal", 4, false, false),
2883 SerializedItem::new("Terminal", 3, true, false),
2884 ],
2885 true,
2886 0,
2887 )),
2888 ],
2889 ),
2890 SerializedPaneGroup::Pane(SerializedPane::new(
2891 vec![
2892 SerializedItem::new("Terminal", 5, true, false),
2893 SerializedItem::new("Terminal", 6, false, false),
2894 ],
2895 false,
2896 0,
2897 )),
2898 ],
2899 );
2900
2901 let workspace = default_workspace(&["/tmp"], ¢er_pane);
2902
2903 db.save_workspace(workspace.clone()).await;
2904
2905 let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
2906
2907 assert_eq!(workspace.center_group, new_workspace.center_group);
2908 }
2909
2910 #[gpui::test]
2911 async fn test_cleanup_panes() {
2912 zlog::init_test();
2913
2914 let db = WorkspaceDb::open_test_db("test_cleanup_panes").await;
2915
2916 let center_pane = group(
2917 Axis::Horizontal,
2918 vec![
2919 group(
2920 Axis::Vertical,
2921 vec![
2922 SerializedPaneGroup::Pane(SerializedPane::new(
2923 vec![
2924 SerializedItem::new("Terminal", 1, false, false),
2925 SerializedItem::new("Terminal", 2, true, false),
2926 ],
2927 false,
2928 0,
2929 )),
2930 SerializedPaneGroup::Pane(SerializedPane::new(
2931 vec![
2932 SerializedItem::new("Terminal", 4, false, false),
2933 SerializedItem::new("Terminal", 3, true, false),
2934 ],
2935 true,
2936 0,
2937 )),
2938 ],
2939 ),
2940 SerializedPaneGroup::Pane(SerializedPane::new(
2941 vec![
2942 SerializedItem::new("Terminal", 5, false, false),
2943 SerializedItem::new("Terminal", 6, true, false),
2944 ],
2945 false,
2946 0,
2947 )),
2948 ],
2949 );
2950
2951 let id = &["/tmp"];
2952
2953 let mut workspace = default_workspace(id, ¢er_pane);
2954
2955 db.save_workspace(workspace.clone()).await;
2956
2957 workspace.center_group = group(
2958 Axis::Vertical,
2959 vec![
2960 SerializedPaneGroup::Pane(SerializedPane::new(
2961 vec![
2962 SerializedItem::new("Terminal", 1, false, false),
2963 SerializedItem::new("Terminal", 2, true, false),
2964 ],
2965 false,
2966 0,
2967 )),
2968 SerializedPaneGroup::Pane(SerializedPane::new(
2969 vec![
2970 SerializedItem::new("Terminal", 4, true, false),
2971 SerializedItem::new("Terminal", 3, false, false),
2972 ],
2973 true,
2974 0,
2975 )),
2976 ],
2977 );
2978
2979 db.save_workspace(workspace.clone()).await;
2980
2981 let new_workspace = db.workspace_for_roots(id).unwrap();
2982
2983 assert_eq!(workspace.center_group, new_workspace.center_group);
2984 }
2985}