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