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