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