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 trusted_workspaces: HashSet<Option<RemoteHostLocation>>,
1821 ) -> anyhow::Result<()> {
1822 use anyhow::Context as _;
1823 use db::sqlez::statement::Statement;
1824 use itertools::Itertools as _;
1825
1826 DB.clear_trusted_worktrees()
1827 .await
1828 .context("clearing previous trust state")?;
1829
1830 let trusted_worktrees = trusted_worktrees
1831 .into_iter()
1832 .flat_map(|(host, abs_paths)| {
1833 abs_paths
1834 .into_iter()
1835 .map(move |abs_path| (Some(abs_path), host.clone()))
1836 })
1837 .chain(trusted_workspaces.into_iter().map(|host| (None, host)))
1838 .collect::<Vec<_>>();
1839 let mut first_worktree;
1840 let mut last_worktree = 0_usize;
1841 for (count, placeholders) in std::iter::once("(?, ?, ?)")
1842 .cycle()
1843 .take(trusted_worktrees.len())
1844 .chunks(MAX_QUERY_PLACEHOLDERS / 3)
1845 .into_iter()
1846 .map(|chunk| {
1847 let mut count = 0;
1848 let placeholders = chunk
1849 .inspect(|_| {
1850 count += 1;
1851 })
1852 .join(", ");
1853 (count, placeholders)
1854 })
1855 .collect::<Vec<_>>()
1856 {
1857 first_worktree = last_worktree;
1858 last_worktree = last_worktree + count;
1859 let query = format!(
1860 r#"INSERT INTO trusted_worktrees(absolute_path, user_name, host_name)
1861VALUES {placeholders};"#
1862 );
1863
1864 let trusted_worktrees = trusted_worktrees[first_worktree..last_worktree].to_vec();
1865 self.write(move |conn| {
1866 let mut statement = Statement::prepare(conn, query)?;
1867 let mut next_index = 1;
1868 for (abs_path, host) in trusted_worktrees {
1869 let abs_path = abs_path.as_ref().map(|abs_path| abs_path.to_string_lossy());
1870 next_index = statement.bind(
1871 &abs_path.as_ref().map(|abs_path| abs_path.as_ref()),
1872 next_index,
1873 )?;
1874 next_index = statement.bind(
1875 &host
1876 .as_ref()
1877 .and_then(|host| Some(host.user_name.as_ref()?.as_str())),
1878 next_index,
1879 )?;
1880 next_index = statement.bind(
1881 &host.as_ref().map(|host| host.host_identifier.as_str()),
1882 next_index,
1883 )?;
1884 }
1885 statement.exec()
1886 })
1887 .await
1888 .context("inserting new trusted state")?;
1889 }
1890 Ok(())
1891 }
1892
1893 pub fn fetch_trusted_worktrees(
1894 &self,
1895 worktree_store: Option<Entity<WorktreeStore>>,
1896 host: Option<RemoteHostLocation>,
1897 cx: &App,
1898 ) -> Result<HashMap<Option<RemoteHostLocation>, HashSet<PathTrust>>> {
1899 let trusted_worktrees = DB.trusted_worktrees()?;
1900 Ok(trusted_worktrees
1901 .into_iter()
1902 .map(|(abs_path, user_name, host_name)| {
1903 let db_host = match (user_name, host_name) {
1904 (_, None) => None,
1905 (None, Some(host_name)) => Some(RemoteHostLocation {
1906 user_name: None,
1907 host_identifier: SharedString::new(host_name),
1908 }),
1909 (Some(user_name), Some(host_name)) => Some(RemoteHostLocation {
1910 user_name: Some(SharedString::new(user_name)),
1911 host_identifier: SharedString::new(host_name),
1912 }),
1913 };
1914
1915 match abs_path {
1916 Some(abs_path) => {
1917 if db_host != host {
1918 (db_host, PathTrust::AbsPath(abs_path))
1919 } else if let Some(worktree_store) = &worktree_store {
1920 find_worktree_in_store(worktree_store.read(cx), &abs_path, cx)
1921 .map(PathTrust::Worktree)
1922 .map(|trusted_worktree| (host.clone(), trusted_worktree))
1923 .unwrap_or_else(|| (db_host.clone(), PathTrust::AbsPath(abs_path)))
1924 } else {
1925 (db_host, PathTrust::AbsPath(abs_path))
1926 }
1927 }
1928 None => (db_host, PathTrust::Workspace),
1929 }
1930 })
1931 .fold(HashMap::default(), |mut acc, (remote_host, path_trust)| {
1932 acc.entry(remote_host)
1933 .or_insert_with(HashSet::default)
1934 .insert(path_trust);
1935 acc
1936 }))
1937 }
1938
1939 query! {
1940 fn trusted_worktrees() -> Result<Vec<(Option<PathBuf>, Option<String>, Option<String>)>> {
1941 SELECT absolute_path, user_name, host_name
1942 FROM trusted_worktrees
1943 }
1944 }
1945
1946 query! {
1947 pub async fn clear_trusted_worktrees() -> Result<()> {
1948 DELETE FROM trusted_worktrees
1949 }
1950 }
1951}
1952
1953pub fn delete_unloaded_items(
1954 alive_items: Vec<ItemId>,
1955 workspace_id: WorkspaceId,
1956 table: &'static str,
1957 db: &ThreadSafeConnection,
1958 cx: &mut App,
1959) -> Task<Result<()>> {
1960 let db = db.clone();
1961 cx.spawn(async move |_| {
1962 let placeholders = alive_items
1963 .iter()
1964 .map(|_| "?")
1965 .collect::<Vec<&str>>()
1966 .join(", ");
1967
1968 let query = format!(
1969 "DELETE FROM {table} WHERE workspace_id = ? AND item_id NOT IN ({placeholders})"
1970 );
1971
1972 db.write(move |conn| {
1973 let mut statement = Statement::prepare(conn, query)?;
1974 let mut next_index = statement.bind(&workspace_id, 1)?;
1975 for id in alive_items {
1976 next_index = statement.bind(&id, next_index)?;
1977 }
1978 statement.exec()
1979 })
1980 .await
1981 })
1982}
1983
1984#[cfg(test)]
1985mod tests {
1986 use super::*;
1987 use crate::persistence::model::{
1988 SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
1989 };
1990 use gpui;
1991 use pretty_assertions::assert_eq;
1992 use remote::SshConnectionOptions;
1993 use std::{thread, time::Duration};
1994
1995 #[gpui::test]
1996 async fn test_breakpoints() {
1997 zlog::init_test();
1998
1999 let db = WorkspaceDb::open_test_db("test_breakpoints").await;
2000 let id = db.next_id().await.unwrap();
2001
2002 let path = Path::new("/tmp/test.rs");
2003
2004 let breakpoint = Breakpoint {
2005 position: 123,
2006 message: None,
2007 state: BreakpointState::Enabled,
2008 condition: None,
2009 hit_condition: None,
2010 };
2011
2012 let log_breakpoint = Breakpoint {
2013 position: 456,
2014 message: Some("Test log message".into()),
2015 state: BreakpointState::Enabled,
2016 condition: None,
2017 hit_condition: None,
2018 };
2019
2020 let disable_breakpoint = Breakpoint {
2021 position: 578,
2022 message: None,
2023 state: BreakpointState::Disabled,
2024 condition: None,
2025 hit_condition: None,
2026 };
2027
2028 let condition_breakpoint = Breakpoint {
2029 position: 789,
2030 message: None,
2031 state: BreakpointState::Enabled,
2032 condition: Some("x > 5".into()),
2033 hit_condition: None,
2034 };
2035
2036 let hit_condition_breakpoint = Breakpoint {
2037 position: 999,
2038 message: None,
2039 state: BreakpointState::Enabled,
2040 condition: None,
2041 hit_condition: Some(">= 3".into()),
2042 };
2043
2044 let workspace = SerializedWorkspace {
2045 id,
2046 paths: PathList::new(&["/tmp"]),
2047 location: SerializedWorkspaceLocation::Local,
2048 center_group: Default::default(),
2049 window_bounds: Default::default(),
2050 display: Default::default(),
2051 docks: Default::default(),
2052 centered_layout: false,
2053 breakpoints: {
2054 let mut map = collections::BTreeMap::default();
2055 map.insert(
2056 Arc::from(path),
2057 vec![
2058 SourceBreakpoint {
2059 row: breakpoint.position,
2060 path: Arc::from(path),
2061 message: breakpoint.message.clone(),
2062 state: breakpoint.state,
2063 condition: breakpoint.condition.clone(),
2064 hit_condition: breakpoint.hit_condition.clone(),
2065 },
2066 SourceBreakpoint {
2067 row: log_breakpoint.position,
2068 path: Arc::from(path),
2069 message: log_breakpoint.message.clone(),
2070 state: log_breakpoint.state,
2071 condition: log_breakpoint.condition.clone(),
2072 hit_condition: log_breakpoint.hit_condition.clone(),
2073 },
2074 SourceBreakpoint {
2075 row: disable_breakpoint.position,
2076 path: Arc::from(path),
2077 message: disable_breakpoint.message.clone(),
2078 state: disable_breakpoint.state,
2079 condition: disable_breakpoint.condition.clone(),
2080 hit_condition: disable_breakpoint.hit_condition.clone(),
2081 },
2082 SourceBreakpoint {
2083 row: condition_breakpoint.position,
2084 path: Arc::from(path),
2085 message: condition_breakpoint.message.clone(),
2086 state: condition_breakpoint.state,
2087 condition: condition_breakpoint.condition.clone(),
2088 hit_condition: condition_breakpoint.hit_condition.clone(),
2089 },
2090 SourceBreakpoint {
2091 row: hit_condition_breakpoint.position,
2092 path: Arc::from(path),
2093 message: hit_condition_breakpoint.message.clone(),
2094 state: hit_condition_breakpoint.state,
2095 condition: hit_condition_breakpoint.condition.clone(),
2096 hit_condition: hit_condition_breakpoint.hit_condition.clone(),
2097 },
2098 ],
2099 );
2100 map
2101 },
2102 session_id: None,
2103 window_id: None,
2104 user_toolchains: Default::default(),
2105 };
2106
2107 db.save_workspace(workspace.clone()).await;
2108
2109 let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
2110 let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(path)).unwrap();
2111
2112 assert_eq!(loaded_breakpoints.len(), 5);
2113
2114 // normal breakpoint
2115 assert_eq!(loaded_breakpoints[0].row, breakpoint.position);
2116 assert_eq!(loaded_breakpoints[0].message, breakpoint.message);
2117 assert_eq!(loaded_breakpoints[0].condition, breakpoint.condition);
2118 assert_eq!(
2119 loaded_breakpoints[0].hit_condition,
2120 breakpoint.hit_condition
2121 );
2122 assert_eq!(loaded_breakpoints[0].state, breakpoint.state);
2123 assert_eq!(loaded_breakpoints[0].path, Arc::from(path));
2124
2125 // enabled breakpoint
2126 assert_eq!(loaded_breakpoints[1].row, log_breakpoint.position);
2127 assert_eq!(loaded_breakpoints[1].message, log_breakpoint.message);
2128 assert_eq!(loaded_breakpoints[1].condition, log_breakpoint.condition);
2129 assert_eq!(
2130 loaded_breakpoints[1].hit_condition,
2131 log_breakpoint.hit_condition
2132 );
2133 assert_eq!(loaded_breakpoints[1].state, log_breakpoint.state);
2134 assert_eq!(loaded_breakpoints[1].path, Arc::from(path));
2135
2136 // disable breakpoint
2137 assert_eq!(loaded_breakpoints[2].row, disable_breakpoint.position);
2138 assert_eq!(loaded_breakpoints[2].message, disable_breakpoint.message);
2139 assert_eq!(
2140 loaded_breakpoints[2].condition,
2141 disable_breakpoint.condition
2142 );
2143 assert_eq!(
2144 loaded_breakpoints[2].hit_condition,
2145 disable_breakpoint.hit_condition
2146 );
2147 assert_eq!(loaded_breakpoints[2].state, disable_breakpoint.state);
2148 assert_eq!(loaded_breakpoints[2].path, Arc::from(path));
2149
2150 // condition breakpoint
2151 assert_eq!(loaded_breakpoints[3].row, condition_breakpoint.position);
2152 assert_eq!(loaded_breakpoints[3].message, condition_breakpoint.message);
2153 assert_eq!(
2154 loaded_breakpoints[3].condition,
2155 condition_breakpoint.condition
2156 );
2157 assert_eq!(
2158 loaded_breakpoints[3].hit_condition,
2159 condition_breakpoint.hit_condition
2160 );
2161 assert_eq!(loaded_breakpoints[3].state, condition_breakpoint.state);
2162 assert_eq!(loaded_breakpoints[3].path, Arc::from(path));
2163
2164 // hit condition breakpoint
2165 assert_eq!(loaded_breakpoints[4].row, hit_condition_breakpoint.position);
2166 assert_eq!(
2167 loaded_breakpoints[4].message,
2168 hit_condition_breakpoint.message
2169 );
2170 assert_eq!(
2171 loaded_breakpoints[4].condition,
2172 hit_condition_breakpoint.condition
2173 );
2174 assert_eq!(
2175 loaded_breakpoints[4].hit_condition,
2176 hit_condition_breakpoint.hit_condition
2177 );
2178 assert_eq!(loaded_breakpoints[4].state, hit_condition_breakpoint.state);
2179 assert_eq!(loaded_breakpoints[4].path, Arc::from(path));
2180 }
2181
2182 #[gpui::test]
2183 async fn test_remove_last_breakpoint() {
2184 zlog::init_test();
2185
2186 let db = WorkspaceDb::open_test_db("test_remove_last_breakpoint").await;
2187 let id = db.next_id().await.unwrap();
2188
2189 let singular_path = Path::new("/tmp/test_remove_last_breakpoint.rs");
2190
2191 let breakpoint_to_remove = Breakpoint {
2192 position: 100,
2193 message: None,
2194 state: BreakpointState::Enabled,
2195 condition: None,
2196 hit_condition: None,
2197 };
2198
2199 let workspace = SerializedWorkspace {
2200 id,
2201 paths: PathList::new(&["/tmp"]),
2202 location: SerializedWorkspaceLocation::Local,
2203 center_group: Default::default(),
2204 window_bounds: Default::default(),
2205 display: Default::default(),
2206 docks: Default::default(),
2207 centered_layout: false,
2208 breakpoints: {
2209 let mut map = collections::BTreeMap::default();
2210 map.insert(
2211 Arc::from(singular_path),
2212 vec![SourceBreakpoint {
2213 row: breakpoint_to_remove.position,
2214 path: Arc::from(singular_path),
2215 message: None,
2216 state: BreakpointState::Enabled,
2217 condition: None,
2218 hit_condition: None,
2219 }],
2220 );
2221 map
2222 },
2223 session_id: None,
2224 window_id: None,
2225 user_toolchains: Default::default(),
2226 };
2227
2228 db.save_workspace(workspace.clone()).await;
2229
2230 let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
2231 let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(singular_path)).unwrap();
2232
2233 assert_eq!(loaded_breakpoints.len(), 1);
2234 assert_eq!(loaded_breakpoints[0].row, breakpoint_to_remove.position);
2235 assert_eq!(loaded_breakpoints[0].message, breakpoint_to_remove.message);
2236 assert_eq!(
2237 loaded_breakpoints[0].condition,
2238 breakpoint_to_remove.condition
2239 );
2240 assert_eq!(
2241 loaded_breakpoints[0].hit_condition,
2242 breakpoint_to_remove.hit_condition
2243 );
2244 assert_eq!(loaded_breakpoints[0].state, breakpoint_to_remove.state);
2245 assert_eq!(loaded_breakpoints[0].path, Arc::from(singular_path));
2246
2247 let workspace_without_breakpoint = SerializedWorkspace {
2248 id,
2249 paths: PathList::new(&["/tmp"]),
2250 location: SerializedWorkspaceLocation::Local,
2251 center_group: Default::default(),
2252 window_bounds: Default::default(),
2253 display: Default::default(),
2254 docks: Default::default(),
2255 centered_layout: false,
2256 breakpoints: collections::BTreeMap::default(),
2257 session_id: None,
2258 window_id: None,
2259 user_toolchains: Default::default(),
2260 };
2261
2262 db.save_workspace(workspace_without_breakpoint.clone())
2263 .await;
2264
2265 let loaded_after_remove = db.workspace_for_roots(&["/tmp"]).unwrap();
2266 let empty_breakpoints = loaded_after_remove
2267 .breakpoints
2268 .get(&Arc::from(singular_path));
2269
2270 assert!(empty_breakpoints.is_none());
2271 }
2272
2273 #[gpui::test]
2274 async fn test_next_id_stability() {
2275 zlog::init_test();
2276
2277 let db = WorkspaceDb::open_test_db("test_next_id_stability").await;
2278
2279 db.write(|conn| {
2280 conn.migrate(
2281 "test_table",
2282 &[sql!(
2283 CREATE TABLE test_table(
2284 text TEXT,
2285 workspace_id INTEGER,
2286 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
2287 ON DELETE CASCADE
2288 ) STRICT;
2289 )],
2290 |_, _, _| false,
2291 )
2292 .unwrap();
2293 })
2294 .await;
2295
2296 let id = db.next_id().await.unwrap();
2297 // Assert the empty row got inserted
2298 assert_eq!(
2299 Some(id),
2300 db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
2301 SELECT workspace_id FROM workspaces WHERE workspace_id = ?
2302 ))
2303 .unwrap()(id)
2304 .unwrap()
2305 );
2306
2307 db.write(move |conn| {
2308 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2309 .unwrap()(("test-text-1", id))
2310 .unwrap()
2311 })
2312 .await;
2313
2314 let test_text_1 = db
2315 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2316 .unwrap()(1)
2317 .unwrap()
2318 .unwrap();
2319 assert_eq!(test_text_1, "test-text-1");
2320 }
2321
2322 #[gpui::test]
2323 async fn test_workspace_id_stability() {
2324 zlog::init_test();
2325
2326 let db = WorkspaceDb::open_test_db("test_workspace_id_stability").await;
2327
2328 db.write(|conn| {
2329 conn.migrate(
2330 "test_table",
2331 &[sql!(
2332 CREATE TABLE test_table(
2333 text TEXT,
2334 workspace_id INTEGER,
2335 FOREIGN KEY(workspace_id)
2336 REFERENCES workspaces(workspace_id)
2337 ON DELETE CASCADE
2338 ) STRICT;)],
2339 |_, _, _| false,
2340 )
2341 })
2342 .await
2343 .unwrap();
2344
2345 let mut workspace_1 = SerializedWorkspace {
2346 id: WorkspaceId(1),
2347 paths: PathList::new(&["/tmp", "/tmp2"]),
2348 location: SerializedWorkspaceLocation::Local,
2349 center_group: Default::default(),
2350 window_bounds: Default::default(),
2351 display: Default::default(),
2352 docks: Default::default(),
2353 centered_layout: false,
2354 breakpoints: Default::default(),
2355 session_id: None,
2356 window_id: None,
2357 user_toolchains: Default::default(),
2358 };
2359
2360 let workspace_2 = SerializedWorkspace {
2361 id: WorkspaceId(2),
2362 paths: PathList::new(&["/tmp"]),
2363 location: SerializedWorkspaceLocation::Local,
2364 center_group: Default::default(),
2365 window_bounds: Default::default(),
2366 display: Default::default(),
2367 docks: Default::default(),
2368 centered_layout: false,
2369 breakpoints: Default::default(),
2370 session_id: None,
2371 window_id: None,
2372 user_toolchains: Default::default(),
2373 };
2374
2375 db.save_workspace(workspace_1.clone()).await;
2376
2377 db.write(|conn| {
2378 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2379 .unwrap()(("test-text-1", 1))
2380 .unwrap();
2381 })
2382 .await;
2383
2384 db.save_workspace(workspace_2.clone()).await;
2385
2386 db.write(|conn| {
2387 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2388 .unwrap()(("test-text-2", 2))
2389 .unwrap();
2390 })
2391 .await;
2392
2393 workspace_1.paths = PathList::new(&["/tmp", "/tmp3"]);
2394 db.save_workspace(workspace_1.clone()).await;
2395 db.save_workspace(workspace_1).await;
2396 db.save_workspace(workspace_2).await;
2397
2398 let test_text_2 = db
2399 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2400 .unwrap()(2)
2401 .unwrap()
2402 .unwrap();
2403 assert_eq!(test_text_2, "test-text-2");
2404
2405 let test_text_1 = db
2406 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2407 .unwrap()(1)
2408 .unwrap()
2409 .unwrap();
2410 assert_eq!(test_text_1, "test-text-1");
2411 }
2412
2413 fn group(axis: Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
2414 SerializedPaneGroup::Group {
2415 axis: SerializedAxis(axis),
2416 flexes: None,
2417 children,
2418 }
2419 }
2420
2421 #[gpui::test]
2422 async fn test_full_workspace_serialization() {
2423 zlog::init_test();
2424
2425 let db = WorkspaceDb::open_test_db("test_full_workspace_serialization").await;
2426
2427 // -----------------
2428 // | 1,2 | 5,6 |
2429 // | - - - | |
2430 // | 3,4 | |
2431 // -----------------
2432 let center_group = group(
2433 Axis::Horizontal,
2434 vec![
2435 group(
2436 Axis::Vertical,
2437 vec![
2438 SerializedPaneGroup::Pane(SerializedPane::new(
2439 vec![
2440 SerializedItem::new("Terminal", 5, false, false),
2441 SerializedItem::new("Terminal", 6, true, false),
2442 ],
2443 false,
2444 0,
2445 )),
2446 SerializedPaneGroup::Pane(SerializedPane::new(
2447 vec![
2448 SerializedItem::new("Terminal", 7, true, false),
2449 SerializedItem::new("Terminal", 8, false, false),
2450 ],
2451 false,
2452 0,
2453 )),
2454 ],
2455 ),
2456 SerializedPaneGroup::Pane(SerializedPane::new(
2457 vec![
2458 SerializedItem::new("Terminal", 9, false, false),
2459 SerializedItem::new("Terminal", 10, true, false),
2460 ],
2461 false,
2462 0,
2463 )),
2464 ],
2465 );
2466
2467 let workspace = SerializedWorkspace {
2468 id: WorkspaceId(5),
2469 paths: PathList::new(&["/tmp", "/tmp2"]),
2470 location: SerializedWorkspaceLocation::Local,
2471 center_group,
2472 window_bounds: Default::default(),
2473 breakpoints: Default::default(),
2474 display: Default::default(),
2475 docks: Default::default(),
2476 centered_layout: false,
2477 session_id: None,
2478 window_id: Some(999),
2479 user_toolchains: Default::default(),
2480 };
2481
2482 db.save_workspace(workspace.clone()).await;
2483
2484 let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
2485 assert_eq!(workspace, round_trip_workspace.unwrap());
2486
2487 // Test guaranteed duplicate IDs
2488 db.save_workspace(workspace.clone()).await;
2489 db.save_workspace(workspace.clone()).await;
2490
2491 let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
2492 assert_eq!(workspace, round_trip_workspace.unwrap());
2493 }
2494
2495 #[gpui::test]
2496 async fn test_workspace_assignment() {
2497 zlog::init_test();
2498
2499 let db = WorkspaceDb::open_test_db("test_basic_functionality").await;
2500
2501 let workspace_1 = SerializedWorkspace {
2502 id: WorkspaceId(1),
2503 paths: PathList::new(&["/tmp", "/tmp2"]),
2504 location: SerializedWorkspaceLocation::Local,
2505 center_group: Default::default(),
2506 window_bounds: Default::default(),
2507 breakpoints: Default::default(),
2508 display: Default::default(),
2509 docks: Default::default(),
2510 centered_layout: false,
2511 session_id: None,
2512 window_id: Some(1),
2513 user_toolchains: Default::default(),
2514 };
2515
2516 let mut workspace_2 = SerializedWorkspace {
2517 id: WorkspaceId(2),
2518 paths: PathList::new(&["/tmp"]),
2519 location: SerializedWorkspaceLocation::Local,
2520 center_group: Default::default(),
2521 window_bounds: Default::default(),
2522 display: Default::default(),
2523 docks: Default::default(),
2524 centered_layout: false,
2525 breakpoints: Default::default(),
2526 session_id: None,
2527 window_id: Some(2),
2528 user_toolchains: Default::default(),
2529 };
2530
2531 db.save_workspace(workspace_1.clone()).await;
2532 db.save_workspace(workspace_2.clone()).await;
2533
2534 // Test that paths are treated as a set
2535 assert_eq!(
2536 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2537 workspace_1
2538 );
2539 assert_eq!(
2540 db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
2541 workspace_1
2542 );
2543
2544 // Make sure that other keys work
2545 assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
2546 assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
2547
2548 // Test 'mutate' case of updating a pre-existing id
2549 workspace_2.paths = PathList::new(&["/tmp", "/tmp2"]);
2550
2551 db.save_workspace(workspace_2.clone()).await;
2552 assert_eq!(
2553 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2554 workspace_2
2555 );
2556
2557 // Test other mechanism for mutating
2558 let mut workspace_3 = SerializedWorkspace {
2559 id: WorkspaceId(3),
2560 paths: PathList::new(&["/tmp2", "/tmp"]),
2561 location: SerializedWorkspaceLocation::Local,
2562 center_group: Default::default(),
2563 window_bounds: Default::default(),
2564 breakpoints: Default::default(),
2565 display: Default::default(),
2566 docks: Default::default(),
2567 centered_layout: false,
2568 session_id: None,
2569 window_id: Some(3),
2570 user_toolchains: Default::default(),
2571 };
2572
2573 db.save_workspace(workspace_3.clone()).await;
2574 assert_eq!(
2575 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2576 workspace_3
2577 );
2578
2579 // Make sure that updating paths differently also works
2580 workspace_3.paths = PathList::new(&["/tmp3", "/tmp4", "/tmp2"]);
2581 db.save_workspace(workspace_3.clone()).await;
2582 assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
2583 assert_eq!(
2584 db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
2585 .unwrap(),
2586 workspace_3
2587 );
2588 }
2589
2590 #[gpui::test]
2591 async fn test_session_workspaces() {
2592 zlog::init_test();
2593
2594 let db = WorkspaceDb::open_test_db("test_serializing_workspaces_session_id").await;
2595
2596 let workspace_1 = SerializedWorkspace {
2597 id: WorkspaceId(1),
2598 paths: PathList::new(&["/tmp1"]),
2599 location: SerializedWorkspaceLocation::Local,
2600 center_group: Default::default(),
2601 window_bounds: Default::default(),
2602 display: Default::default(),
2603 docks: Default::default(),
2604 centered_layout: false,
2605 breakpoints: Default::default(),
2606 session_id: Some("session-id-1".to_owned()),
2607 window_id: Some(10),
2608 user_toolchains: Default::default(),
2609 };
2610
2611 let workspace_2 = SerializedWorkspace {
2612 id: WorkspaceId(2),
2613 paths: PathList::new(&["/tmp2"]),
2614 location: SerializedWorkspaceLocation::Local,
2615 center_group: Default::default(),
2616 window_bounds: Default::default(),
2617 display: Default::default(),
2618 docks: Default::default(),
2619 centered_layout: false,
2620 breakpoints: Default::default(),
2621 session_id: Some("session-id-1".to_owned()),
2622 window_id: Some(20),
2623 user_toolchains: Default::default(),
2624 };
2625
2626 let workspace_3 = SerializedWorkspace {
2627 id: WorkspaceId(3),
2628 paths: PathList::new(&["/tmp3"]),
2629 location: SerializedWorkspaceLocation::Local,
2630 center_group: Default::default(),
2631 window_bounds: Default::default(),
2632 display: Default::default(),
2633 docks: Default::default(),
2634 centered_layout: false,
2635 breakpoints: Default::default(),
2636 session_id: Some("session-id-2".to_owned()),
2637 window_id: Some(30),
2638 user_toolchains: Default::default(),
2639 };
2640
2641 let workspace_4 = SerializedWorkspace {
2642 id: WorkspaceId(4),
2643 paths: PathList::new(&["/tmp4"]),
2644 location: SerializedWorkspaceLocation::Local,
2645 center_group: Default::default(),
2646 window_bounds: Default::default(),
2647 display: Default::default(),
2648 docks: Default::default(),
2649 centered_layout: false,
2650 breakpoints: Default::default(),
2651 session_id: None,
2652 window_id: None,
2653 user_toolchains: Default::default(),
2654 };
2655
2656 let connection_id = db
2657 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2658 host: "my-host".to_string(),
2659 port: Some(1234),
2660 ..Default::default()
2661 }))
2662 .await
2663 .unwrap();
2664
2665 let workspace_5 = SerializedWorkspace {
2666 id: WorkspaceId(5),
2667 paths: PathList::default(),
2668 location: SerializedWorkspaceLocation::Remote(
2669 db.remote_connection(connection_id).unwrap(),
2670 ),
2671 center_group: Default::default(),
2672 window_bounds: Default::default(),
2673 display: Default::default(),
2674 docks: Default::default(),
2675 centered_layout: false,
2676 breakpoints: Default::default(),
2677 session_id: Some("session-id-2".to_owned()),
2678 window_id: Some(50),
2679 user_toolchains: Default::default(),
2680 };
2681
2682 let workspace_6 = SerializedWorkspace {
2683 id: WorkspaceId(6),
2684 paths: PathList::new(&["/tmp6c", "/tmp6b", "/tmp6a"]),
2685 location: SerializedWorkspaceLocation::Local,
2686 center_group: Default::default(),
2687 window_bounds: Default::default(),
2688 breakpoints: Default::default(),
2689 display: Default::default(),
2690 docks: Default::default(),
2691 centered_layout: false,
2692 session_id: Some("session-id-3".to_owned()),
2693 window_id: Some(60),
2694 user_toolchains: Default::default(),
2695 };
2696
2697 db.save_workspace(workspace_1.clone()).await;
2698 thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
2699 db.save_workspace(workspace_2.clone()).await;
2700 db.save_workspace(workspace_3.clone()).await;
2701 thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
2702 db.save_workspace(workspace_4.clone()).await;
2703 db.save_workspace(workspace_5.clone()).await;
2704 db.save_workspace(workspace_6.clone()).await;
2705
2706 let locations = db.session_workspaces("session-id-1".to_owned()).unwrap();
2707 assert_eq!(locations.len(), 2);
2708 assert_eq!(locations[0].0, PathList::new(&["/tmp2"]));
2709 assert_eq!(locations[0].1, Some(20));
2710 assert_eq!(locations[1].0, PathList::new(&["/tmp1"]));
2711 assert_eq!(locations[1].1, Some(10));
2712
2713 let locations = db.session_workspaces("session-id-2".to_owned()).unwrap();
2714 assert_eq!(locations.len(), 2);
2715 assert_eq!(locations[0].0, PathList::default());
2716 assert_eq!(locations[0].1, Some(50));
2717 assert_eq!(locations[0].2, Some(connection_id));
2718 assert_eq!(locations[1].0, PathList::new(&["/tmp3"]));
2719 assert_eq!(locations[1].1, Some(30));
2720
2721 let locations = db.session_workspaces("session-id-3".to_owned()).unwrap();
2722 assert_eq!(locations.len(), 1);
2723 assert_eq!(
2724 locations[0].0,
2725 PathList::new(&["/tmp6c", "/tmp6b", "/tmp6a"]),
2726 );
2727 assert_eq!(locations[0].1, Some(60));
2728 }
2729
2730 fn default_workspace<P: AsRef<Path>>(
2731 paths: &[P],
2732 center_group: &SerializedPaneGroup,
2733 ) -> SerializedWorkspace {
2734 SerializedWorkspace {
2735 id: WorkspaceId(4),
2736 paths: PathList::new(paths),
2737 location: SerializedWorkspaceLocation::Local,
2738 center_group: center_group.clone(),
2739 window_bounds: Default::default(),
2740 display: Default::default(),
2741 docks: Default::default(),
2742 breakpoints: Default::default(),
2743 centered_layout: false,
2744 session_id: None,
2745 window_id: None,
2746 user_toolchains: Default::default(),
2747 }
2748 }
2749
2750 #[gpui::test]
2751 async fn test_last_session_workspace_locations() {
2752 let dir1 = tempfile::TempDir::with_prefix("dir1").unwrap();
2753 let dir2 = tempfile::TempDir::with_prefix("dir2").unwrap();
2754 let dir3 = tempfile::TempDir::with_prefix("dir3").unwrap();
2755 let dir4 = tempfile::TempDir::with_prefix("dir4").unwrap();
2756
2757 let db =
2758 WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces").await;
2759
2760 let workspaces = [
2761 (1, vec![dir1.path()], 9),
2762 (2, vec![dir2.path()], 5),
2763 (3, vec![dir3.path()], 8),
2764 (4, vec![dir4.path()], 2),
2765 (5, vec![dir1.path(), dir2.path(), dir3.path()], 3),
2766 (6, vec![dir4.path(), dir3.path(), dir2.path()], 4),
2767 ]
2768 .into_iter()
2769 .map(|(id, paths, window_id)| SerializedWorkspace {
2770 id: WorkspaceId(id),
2771 paths: PathList::new(paths.as_slice()),
2772 location: SerializedWorkspaceLocation::Local,
2773 center_group: Default::default(),
2774 window_bounds: Default::default(),
2775 display: Default::default(),
2776 docks: Default::default(),
2777 centered_layout: false,
2778 session_id: Some("one-session".to_owned()),
2779 breakpoints: Default::default(),
2780 window_id: Some(window_id),
2781 user_toolchains: Default::default(),
2782 })
2783 .collect::<Vec<_>>();
2784
2785 for workspace in workspaces.iter() {
2786 db.save_workspace(workspace.clone()).await;
2787 }
2788
2789 let stack = Some(Vec::from([
2790 WindowId::from(2), // Top
2791 WindowId::from(8),
2792 WindowId::from(5),
2793 WindowId::from(9),
2794 WindowId::from(3),
2795 WindowId::from(4), // Bottom
2796 ]));
2797
2798 let locations = db
2799 .last_session_workspace_locations("one-session", stack)
2800 .unwrap();
2801 assert_eq!(
2802 locations,
2803 [
2804 (
2805 SerializedWorkspaceLocation::Local,
2806 PathList::new(&[dir4.path()])
2807 ),
2808 (
2809 SerializedWorkspaceLocation::Local,
2810 PathList::new(&[dir3.path()])
2811 ),
2812 (
2813 SerializedWorkspaceLocation::Local,
2814 PathList::new(&[dir2.path()])
2815 ),
2816 (
2817 SerializedWorkspaceLocation::Local,
2818 PathList::new(&[dir1.path()])
2819 ),
2820 (
2821 SerializedWorkspaceLocation::Local,
2822 PathList::new(&[dir1.path(), dir2.path(), dir3.path()])
2823 ),
2824 (
2825 SerializedWorkspaceLocation::Local,
2826 PathList::new(&[dir4.path(), dir3.path(), dir2.path()])
2827 ),
2828 ]
2829 );
2830 }
2831
2832 #[gpui::test]
2833 async fn test_last_session_workspace_locations_remote() {
2834 let db =
2835 WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces_remote")
2836 .await;
2837
2838 let remote_connections = [
2839 ("host-1", "my-user-1"),
2840 ("host-2", "my-user-2"),
2841 ("host-3", "my-user-3"),
2842 ("host-4", "my-user-4"),
2843 ]
2844 .into_iter()
2845 .map(|(host, user)| async {
2846 let options = RemoteConnectionOptions::Ssh(SshConnectionOptions {
2847 host: host.to_string(),
2848 username: Some(user.to_string()),
2849 ..Default::default()
2850 });
2851 db.get_or_create_remote_connection(options.clone())
2852 .await
2853 .unwrap();
2854 options
2855 })
2856 .collect::<Vec<_>>();
2857
2858 let remote_connections = futures::future::join_all(remote_connections).await;
2859
2860 let workspaces = [
2861 (1, remote_connections[0].clone(), 9),
2862 (2, remote_connections[1].clone(), 5),
2863 (3, remote_connections[2].clone(), 8),
2864 (4, remote_connections[3].clone(), 2),
2865 ]
2866 .into_iter()
2867 .map(|(id, remote_connection, window_id)| SerializedWorkspace {
2868 id: WorkspaceId(id),
2869 paths: PathList::default(),
2870 location: SerializedWorkspaceLocation::Remote(remote_connection),
2871 center_group: Default::default(),
2872 window_bounds: Default::default(),
2873 display: Default::default(),
2874 docks: Default::default(),
2875 centered_layout: false,
2876 session_id: Some("one-session".to_owned()),
2877 breakpoints: Default::default(),
2878 window_id: Some(window_id),
2879 user_toolchains: Default::default(),
2880 })
2881 .collect::<Vec<_>>();
2882
2883 for workspace in workspaces.iter() {
2884 db.save_workspace(workspace.clone()).await;
2885 }
2886
2887 let stack = Some(Vec::from([
2888 WindowId::from(2), // Top
2889 WindowId::from(8),
2890 WindowId::from(5),
2891 WindowId::from(9), // Bottom
2892 ]));
2893
2894 let have = db
2895 .last_session_workspace_locations("one-session", stack)
2896 .unwrap();
2897 assert_eq!(have.len(), 4);
2898 assert_eq!(
2899 have[0],
2900 (
2901 SerializedWorkspaceLocation::Remote(remote_connections[3].clone()),
2902 PathList::default()
2903 )
2904 );
2905 assert_eq!(
2906 have[1],
2907 (
2908 SerializedWorkspaceLocation::Remote(remote_connections[2].clone()),
2909 PathList::default()
2910 )
2911 );
2912 assert_eq!(
2913 have[2],
2914 (
2915 SerializedWorkspaceLocation::Remote(remote_connections[1].clone()),
2916 PathList::default()
2917 )
2918 );
2919 assert_eq!(
2920 have[3],
2921 (
2922 SerializedWorkspaceLocation::Remote(remote_connections[0].clone()),
2923 PathList::default()
2924 )
2925 );
2926 }
2927
2928 #[gpui::test]
2929 async fn test_get_or_create_ssh_project() {
2930 let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project").await;
2931
2932 let host = "example.com".to_string();
2933 let port = Some(22_u16);
2934 let user = Some("user".to_string());
2935
2936 let connection_id = db
2937 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2938 host: host.clone(),
2939 port,
2940 username: user.clone(),
2941 ..Default::default()
2942 }))
2943 .await
2944 .unwrap();
2945
2946 // Test that calling the function again with the same parameters returns the same project
2947 let same_connection = db
2948 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2949 host: host.clone(),
2950 port,
2951 username: user.clone(),
2952 ..Default::default()
2953 }))
2954 .await
2955 .unwrap();
2956
2957 assert_eq!(connection_id, same_connection);
2958
2959 // Test with different parameters
2960 let host2 = "otherexample.com".to_string();
2961 let port2 = None;
2962 let user2 = Some("otheruser".to_string());
2963
2964 let different_connection = db
2965 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2966 host: host2.clone(),
2967 port: port2,
2968 username: user2.clone(),
2969 ..Default::default()
2970 }))
2971 .await
2972 .unwrap();
2973
2974 assert_ne!(connection_id, different_connection);
2975 }
2976
2977 #[gpui::test]
2978 async fn test_get_or_create_ssh_project_with_null_user() {
2979 let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project_with_null_user").await;
2980
2981 let (host, port, user) = ("example.com".to_string(), None, None);
2982
2983 let connection_id = db
2984 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2985 host: host.clone(),
2986 port,
2987 username: None,
2988 ..Default::default()
2989 }))
2990 .await
2991 .unwrap();
2992
2993 let same_connection_id = db
2994 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2995 host: host.clone(),
2996 port,
2997 username: user.clone(),
2998 ..Default::default()
2999 }))
3000 .await
3001 .unwrap();
3002
3003 assert_eq!(connection_id, same_connection_id);
3004 }
3005
3006 #[gpui::test]
3007 async fn test_get_remote_connections() {
3008 let db = WorkspaceDb::open_test_db("test_get_remote_connections").await;
3009
3010 let connections = [
3011 ("example.com".to_string(), None, None),
3012 (
3013 "anotherexample.com".to_string(),
3014 Some(123_u16),
3015 Some("user2".to_string()),
3016 ),
3017 ("yetanother.com".to_string(), Some(345_u16), None),
3018 ];
3019
3020 let mut ids = Vec::new();
3021 for (host, port, user) in connections.iter() {
3022 ids.push(
3023 db.get_or_create_remote_connection(RemoteConnectionOptions::Ssh(
3024 SshConnectionOptions {
3025 host: host.clone(),
3026 port: *port,
3027 username: user.clone(),
3028 ..Default::default()
3029 },
3030 ))
3031 .await
3032 .unwrap(),
3033 );
3034 }
3035
3036 let stored_connections = db.remote_connections().unwrap();
3037 assert_eq!(
3038 stored_connections,
3039 [
3040 (
3041 ids[0],
3042 RemoteConnectionOptions::Ssh(SshConnectionOptions {
3043 host: "example.com".into(),
3044 port: None,
3045 username: None,
3046 ..Default::default()
3047 }),
3048 ),
3049 (
3050 ids[1],
3051 RemoteConnectionOptions::Ssh(SshConnectionOptions {
3052 host: "anotherexample.com".into(),
3053 port: Some(123),
3054 username: Some("user2".into()),
3055 ..Default::default()
3056 }),
3057 ),
3058 (
3059 ids[2],
3060 RemoteConnectionOptions::Ssh(SshConnectionOptions {
3061 host: "yetanother.com".into(),
3062 port: Some(345),
3063 username: None,
3064 ..Default::default()
3065 }),
3066 ),
3067 ]
3068 .into_iter()
3069 .collect::<HashMap<_, _>>(),
3070 );
3071 }
3072
3073 #[gpui::test]
3074 async fn test_simple_split() {
3075 zlog::init_test();
3076
3077 let db = WorkspaceDb::open_test_db("simple_split").await;
3078
3079 // -----------------
3080 // | 1,2 | 5,6 |
3081 // | - - - | |
3082 // | 3,4 | |
3083 // -----------------
3084 let center_pane = group(
3085 Axis::Horizontal,
3086 vec![
3087 group(
3088 Axis::Vertical,
3089 vec![
3090 SerializedPaneGroup::Pane(SerializedPane::new(
3091 vec![
3092 SerializedItem::new("Terminal", 1, false, false),
3093 SerializedItem::new("Terminal", 2, true, false),
3094 ],
3095 false,
3096 0,
3097 )),
3098 SerializedPaneGroup::Pane(SerializedPane::new(
3099 vec![
3100 SerializedItem::new("Terminal", 4, false, false),
3101 SerializedItem::new("Terminal", 3, true, false),
3102 ],
3103 true,
3104 0,
3105 )),
3106 ],
3107 ),
3108 SerializedPaneGroup::Pane(SerializedPane::new(
3109 vec![
3110 SerializedItem::new("Terminal", 5, true, false),
3111 SerializedItem::new("Terminal", 6, false, false),
3112 ],
3113 false,
3114 0,
3115 )),
3116 ],
3117 );
3118
3119 let workspace = default_workspace(&["/tmp"], ¢er_pane);
3120
3121 db.save_workspace(workspace.clone()).await;
3122
3123 let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
3124
3125 assert_eq!(workspace.center_group, new_workspace.center_group);
3126 }
3127
3128 #[gpui::test]
3129 async fn test_cleanup_panes() {
3130 zlog::init_test();
3131
3132 let db = WorkspaceDb::open_test_db("test_cleanup_panes").await;
3133
3134 let center_pane = group(
3135 Axis::Horizontal,
3136 vec![
3137 group(
3138 Axis::Vertical,
3139 vec![
3140 SerializedPaneGroup::Pane(SerializedPane::new(
3141 vec![
3142 SerializedItem::new("Terminal", 1, false, false),
3143 SerializedItem::new("Terminal", 2, true, false),
3144 ],
3145 false,
3146 0,
3147 )),
3148 SerializedPaneGroup::Pane(SerializedPane::new(
3149 vec![
3150 SerializedItem::new("Terminal", 4, false, false),
3151 SerializedItem::new("Terminal", 3, true, false),
3152 ],
3153 true,
3154 0,
3155 )),
3156 ],
3157 ),
3158 SerializedPaneGroup::Pane(SerializedPane::new(
3159 vec![
3160 SerializedItem::new("Terminal", 5, false, false),
3161 SerializedItem::new("Terminal", 6, true, false),
3162 ],
3163 false,
3164 0,
3165 )),
3166 ],
3167 );
3168
3169 let id = &["/tmp"];
3170
3171 let mut workspace = default_workspace(id, ¢er_pane);
3172
3173 db.save_workspace(workspace.clone()).await;
3174
3175 workspace.center_group = group(
3176 Axis::Vertical,
3177 vec![
3178 SerializedPaneGroup::Pane(SerializedPane::new(
3179 vec![
3180 SerializedItem::new("Terminal", 1, false, false),
3181 SerializedItem::new("Terminal", 2, true, false),
3182 ],
3183 false,
3184 0,
3185 )),
3186 SerializedPaneGroup::Pane(SerializedPane::new(
3187 vec![
3188 SerializedItem::new("Terminal", 4, true, false),
3189 SerializedItem::new("Terminal", 3, false, false),
3190 ],
3191 true,
3192 0,
3193 )),
3194 ],
3195 );
3196
3197 db.save_workspace(workspace.clone()).await;
3198
3199 let new_workspace = db.workspace_for_roots(id).unwrap();
3200
3201 assert_eq!(workspace.center_group, new_workspace.center_group);
3202 }
3203}