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