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