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