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