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