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 DELETE FROM toolchains WHERE workspace_id = ?1;
828 )
829 )?(workspace.id).context("Clearing old breakpoints")?;
830
831 for (path, breakpoints) in workspace.breakpoints {
832 for bp in breakpoints {
833 let state = BreakpointStateWrapper::from(bp.state);
834 match conn.exec_bound(sql!(
835 INSERT INTO breakpoints (workspace_id, path, breakpoint_location, log_message, condition, hit_condition, state)
836 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7);))?
837
838 ((
839 workspace.id,
840 path.as_ref(),
841 bp.row,
842 bp.message,
843 bp.condition,
844 bp.hit_condition,
845 state,
846 )) {
847 Ok(_) => {
848 log::debug!("Stored breakpoint at row: {} in path: {}", bp.row, path.to_string_lossy())
849 }
850 Err(err) => {
851 log::error!("{err}");
852 continue;
853 }
854 }
855 }
856 }
857
858 conn.exec_bound(sql!(
859 DELETE
860 FROM workspaces
861 WHERE
862 workspace_id != ?1 AND
863 paths IS ?2 AND
864 ssh_connection_id IS ?3
865 ))?((
866 workspace.id,
867 paths.paths.clone(),
868 ssh_connection_id,
869 ))
870 .context("clearing out old locations")?;
871
872 // Upsert
873 let query = sql!(
874 INSERT INTO workspaces(
875 workspace_id,
876 paths,
877 paths_order,
878 ssh_connection_id,
879 left_dock_visible,
880 left_dock_active_panel,
881 left_dock_zoom,
882 right_dock_visible,
883 right_dock_active_panel,
884 right_dock_zoom,
885 bottom_dock_visible,
886 bottom_dock_active_panel,
887 bottom_dock_zoom,
888 session_id,
889 window_id,
890 timestamp
891 )
892 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, CURRENT_TIMESTAMP)
893 ON CONFLICT DO
894 UPDATE SET
895 paths = ?2,
896 paths_order = ?3,
897 ssh_connection_id = ?4,
898 left_dock_visible = ?5,
899 left_dock_active_panel = ?6,
900 left_dock_zoom = ?7,
901 right_dock_visible = ?8,
902 right_dock_active_panel = ?9,
903 right_dock_zoom = ?10,
904 bottom_dock_visible = ?11,
905 bottom_dock_active_panel = ?12,
906 bottom_dock_zoom = ?13,
907 session_id = ?14,
908 window_id = ?15,
909 timestamp = CURRENT_TIMESTAMP
910 );
911 let mut prepared_query = conn.exec_bound(query)?;
912 let args = (
913 workspace.id,
914 paths.paths.clone(),
915 paths.order.clone(),
916 ssh_connection_id,
917 workspace.docks,
918 workspace.session_id,
919 workspace.window_id,
920 );
921
922 prepared_query(args).context("Updating workspace")?;
923
924 // Save center pane group
925 Self::save_pane_group(conn, workspace.id, &workspace.center_group, None)
926 .context("save pane group in save workspace")?;
927
928 Ok(())
929 })
930 .log_err();
931 })
932 .await;
933 }
934
935 pub(crate) async fn get_or_create_ssh_connection(
936 &self,
937 host: String,
938 port: Option<u16>,
939 user: Option<String>,
940 ) -> Result<SshConnectionId> {
941 self.write(move |conn| Self::get_or_create_ssh_connection_query(conn, host, port, user))
942 .await
943 }
944
945 fn get_or_create_ssh_connection_query(
946 this: &Connection,
947 host: String,
948 port: Option<u16>,
949 user: Option<String>,
950 ) -> Result<SshConnectionId> {
951 if let Some(id) = this.select_row_bound(sql!(
952 SELECT id FROM ssh_connections WHERE host IS ? AND port IS ? AND user IS ? LIMIT 1
953 ))?((host.clone(), port, user.clone()))?
954 {
955 Ok(SshConnectionId(id))
956 } else {
957 log::debug!("Inserting SSH project at host {host}");
958 let id = this.select_row_bound(sql!(
959 INSERT INTO ssh_connections (
960 host,
961 port,
962 user
963 ) VALUES (?1, ?2, ?3)
964 RETURNING id
965 ))?((host, port, user))?
966 .context("failed to insert ssh project")?;
967 Ok(SshConnectionId(id))
968 }
969 }
970
971 query! {
972 pub async fn next_id() -> Result<WorkspaceId> {
973 INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id
974 }
975 }
976
977 fn recent_workspaces(&self) -> Result<Vec<(WorkspaceId, PathList, Option<u64>)>> {
978 Ok(self
979 .recent_workspaces_query()?
980 .into_iter()
981 .map(|(id, paths, order, ssh_connection_id)| {
982 (
983 id,
984 PathList::deserialize(&SerializedPathList { paths, order }),
985 ssh_connection_id,
986 )
987 })
988 .collect())
989 }
990
991 query! {
992 fn recent_workspaces_query() -> Result<Vec<(WorkspaceId, String, String, Option<u64>)>> {
993 SELECT workspace_id, paths, paths_order, ssh_connection_id
994 FROM workspaces
995 WHERE
996 paths IS NOT NULL OR
997 ssh_connection_id IS NOT NULL
998 ORDER BY timestamp DESC
999 }
1000 }
1001
1002 fn session_workspaces(
1003 &self,
1004 session_id: String,
1005 ) -> Result<Vec<(PathList, Option<u64>, Option<SshConnectionId>)>> {
1006 Ok(self
1007 .session_workspaces_query(session_id)?
1008 .into_iter()
1009 .map(|(paths, order, window_id, ssh_connection_id)| {
1010 (
1011 PathList::deserialize(&SerializedPathList { paths, order }),
1012 window_id,
1013 ssh_connection_id.map(SshConnectionId),
1014 )
1015 })
1016 .collect())
1017 }
1018
1019 query! {
1020 fn session_workspaces_query(session_id: String) -> Result<Vec<(String, String, Option<u64>, Option<u64>)>> {
1021 SELECT paths, paths_order, window_id, ssh_connection_id
1022 FROM workspaces
1023 WHERE session_id = ?1
1024 ORDER BY timestamp DESC
1025 }
1026 }
1027
1028 query! {
1029 pub fn breakpoints_for_file(workspace_id: WorkspaceId, file_path: &Path) -> Result<Vec<Breakpoint>> {
1030 SELECT breakpoint_location
1031 FROM breakpoints
1032 WHERE workspace_id= ?1 AND path = ?2
1033 }
1034 }
1035
1036 query! {
1037 pub fn clear_breakpoints(file_path: &Path) -> Result<()> {
1038 DELETE FROM breakpoints
1039 WHERE file_path = ?2
1040 }
1041 }
1042
1043 fn ssh_connections(&self) -> Result<HashMap<SshConnectionId, SerializedSshConnection>> {
1044 Ok(self
1045 .ssh_connections_query()?
1046 .into_iter()
1047 .map(|(id, host, port, user)| {
1048 (
1049 SshConnectionId(id),
1050 SerializedSshConnection { host, port, user },
1051 )
1052 })
1053 .collect())
1054 }
1055
1056 query! {
1057 pub fn ssh_connections_query() -> Result<Vec<(u64, String, Option<u16>, Option<String>)>> {
1058 SELECT id, host, port, user
1059 FROM ssh_connections
1060 }
1061 }
1062
1063 pub(crate) fn ssh_connection(&self, id: SshConnectionId) -> Result<SerializedSshConnection> {
1064 let row = self.ssh_connection_query(id.0)?;
1065 Ok(SerializedSshConnection {
1066 host: row.0,
1067 port: row.1,
1068 user: row.2,
1069 })
1070 }
1071
1072 query! {
1073 fn ssh_connection_query(id: u64) -> Result<(String, Option<u16>, Option<String>)> {
1074 SELECT host, port, user
1075 FROM ssh_connections
1076 WHERE id = ?
1077 }
1078 }
1079
1080 pub(crate) fn last_window(
1081 &self,
1082 ) -> anyhow::Result<(Option<Uuid>, Option<SerializedWindowBounds>)> {
1083 let mut prepared_query =
1084 self.select::<(Option<Uuid>, Option<SerializedWindowBounds>)>(sql!(
1085 SELECT
1086 display,
1087 window_state, window_x, window_y, window_width, window_height
1088 FROM workspaces
1089 WHERE paths
1090 IS NOT NULL
1091 ORDER BY timestamp DESC
1092 LIMIT 1
1093 ))?;
1094 let result = prepared_query()?;
1095 Ok(result.into_iter().next().unwrap_or((None, None)))
1096 }
1097
1098 query! {
1099 pub async fn delete_workspace_by_id(id: WorkspaceId) -> Result<()> {
1100 DELETE FROM toolchains WHERE workspace_id = ?1;
1101 DELETE FROM workspaces
1102 WHERE workspace_id IS ?
1103 }
1104 }
1105
1106 // Returns the recent locations which are still valid on disk and deletes ones which no longer
1107 // exist.
1108 pub async fn recent_workspaces_on_disk(
1109 &self,
1110 ) -> Result<Vec<(WorkspaceId, SerializedWorkspaceLocation, PathList)>> {
1111 let mut result = Vec::new();
1112 let mut delete_tasks = Vec::new();
1113 let ssh_connections = self.ssh_connections()?;
1114
1115 for (id, paths, ssh_connection_id) in self.recent_workspaces()? {
1116 if let Some(ssh_connection_id) = ssh_connection_id.map(SshConnectionId) {
1117 if let Some(ssh_connection) = ssh_connections.get(&ssh_connection_id) {
1118 result.push((
1119 id,
1120 SerializedWorkspaceLocation::Ssh(ssh_connection.clone()),
1121 paths,
1122 ));
1123 } else {
1124 delete_tasks.push(self.delete_workspace_by_id(id));
1125 }
1126 continue;
1127 }
1128
1129 if paths.paths().iter().all(|path| path.exists())
1130 && paths.paths().iter().any(|path| path.is_dir())
1131 {
1132 result.push((id, SerializedWorkspaceLocation::Local, paths));
1133 } else {
1134 delete_tasks.push(self.delete_workspace_by_id(id));
1135 }
1136 }
1137
1138 futures::future::join_all(delete_tasks).await;
1139 Ok(result)
1140 }
1141
1142 pub async fn last_workspace(&self) -> Result<Option<(SerializedWorkspaceLocation, PathList)>> {
1143 Ok(self
1144 .recent_workspaces_on_disk()
1145 .await?
1146 .into_iter()
1147 .next()
1148 .map(|(_, location, paths)| (location, paths)))
1149 }
1150
1151 // Returns the locations of the workspaces that were still opened when the last
1152 // session was closed (i.e. when Zed was quit).
1153 // If `last_session_window_order` is provided, the returned locations are ordered
1154 // according to that.
1155 pub fn last_session_workspace_locations(
1156 &self,
1157 last_session_id: &str,
1158 last_session_window_stack: Option<Vec<WindowId>>,
1159 ) -> Result<Vec<(SerializedWorkspaceLocation, PathList)>> {
1160 let mut workspaces = Vec::new();
1161
1162 for (paths, window_id, ssh_connection_id) in
1163 self.session_workspaces(last_session_id.to_owned())?
1164 {
1165 if let Some(ssh_connection_id) = ssh_connection_id {
1166 workspaces.push((
1167 SerializedWorkspaceLocation::Ssh(self.ssh_connection(ssh_connection_id)?),
1168 paths,
1169 window_id.map(WindowId::from),
1170 ));
1171 } else if paths.paths().iter().all(|path| path.exists())
1172 && paths.paths().iter().any(|path| path.is_dir())
1173 {
1174 workspaces.push((
1175 SerializedWorkspaceLocation::Local,
1176 paths,
1177 window_id.map(WindowId::from),
1178 ));
1179 }
1180 }
1181
1182 if let Some(stack) = last_session_window_stack {
1183 workspaces.sort_by_key(|(_, _, window_id)| {
1184 window_id
1185 .and_then(|id| stack.iter().position(|&order_id| order_id == id))
1186 .unwrap_or(usize::MAX)
1187 });
1188 }
1189
1190 Ok(workspaces
1191 .into_iter()
1192 .map(|(location, paths, _)| (location, paths))
1193 .collect::<Vec<_>>())
1194 }
1195
1196 fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
1197 Ok(self
1198 .get_pane_group(workspace_id, None)?
1199 .into_iter()
1200 .next()
1201 .unwrap_or_else(|| {
1202 SerializedPaneGroup::Pane(SerializedPane {
1203 active: true,
1204 children: vec![],
1205 pinned_count: 0,
1206 })
1207 }))
1208 }
1209
1210 fn get_pane_group(
1211 &self,
1212 workspace_id: WorkspaceId,
1213 group_id: Option<GroupId>,
1214 ) -> Result<Vec<SerializedPaneGroup>> {
1215 type GroupKey = (Option<GroupId>, WorkspaceId);
1216 type GroupOrPane = (
1217 Option<GroupId>,
1218 Option<SerializedAxis>,
1219 Option<PaneId>,
1220 Option<bool>,
1221 Option<usize>,
1222 Option<String>,
1223 );
1224 self.select_bound::<GroupKey, GroupOrPane>(sql!(
1225 SELECT group_id, axis, pane_id, active, pinned_count, flexes
1226 FROM (SELECT
1227 group_id,
1228 axis,
1229 NULL as pane_id,
1230 NULL as active,
1231 NULL as pinned_count,
1232 position,
1233 parent_group_id,
1234 workspace_id,
1235 flexes
1236 FROM pane_groups
1237 UNION
1238 SELECT
1239 NULL,
1240 NULL,
1241 center_panes.pane_id,
1242 panes.active as active,
1243 pinned_count,
1244 position,
1245 parent_group_id,
1246 panes.workspace_id as workspace_id,
1247 NULL
1248 FROM center_panes
1249 JOIN panes ON center_panes.pane_id = panes.pane_id)
1250 WHERE parent_group_id IS ? AND workspace_id = ?
1251 ORDER BY position
1252 ))?((group_id, workspace_id))?
1253 .into_iter()
1254 .map(|(group_id, axis, pane_id, active, pinned_count, flexes)| {
1255 let maybe_pane = maybe!({ Some((pane_id?, active?, pinned_count?)) });
1256 if let Some((group_id, axis)) = group_id.zip(axis) {
1257 let flexes = flexes
1258 .map(|flexes: String| serde_json::from_str::<Vec<f32>>(&flexes))
1259 .transpose()?;
1260
1261 Ok(SerializedPaneGroup::Group {
1262 axis,
1263 children: self.get_pane_group(workspace_id, Some(group_id))?,
1264 flexes,
1265 })
1266 } else if let Some((pane_id, active, pinned_count)) = maybe_pane {
1267 Ok(SerializedPaneGroup::Pane(SerializedPane::new(
1268 self.get_items(pane_id)?,
1269 active,
1270 pinned_count,
1271 )))
1272 } else {
1273 bail!("Pane Group Child was neither a pane group or a pane");
1274 }
1275 })
1276 // Filter out panes and pane groups which don't have any children or items
1277 .filter(|pane_group| match pane_group {
1278 Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
1279 Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
1280 _ => true,
1281 })
1282 .collect::<Result<_>>()
1283 }
1284
1285 fn save_pane_group(
1286 conn: &Connection,
1287 workspace_id: WorkspaceId,
1288 pane_group: &SerializedPaneGroup,
1289 parent: Option<(GroupId, usize)>,
1290 ) -> Result<()> {
1291 if parent.is_none() {
1292 log::debug!("Saving a pane group for workspace {workspace_id:?}");
1293 }
1294 match pane_group {
1295 SerializedPaneGroup::Group {
1296 axis,
1297 children,
1298 flexes,
1299 } => {
1300 let (parent_id, position) = parent.unzip();
1301
1302 let flex_string = flexes
1303 .as_ref()
1304 .map(|flexes| serde_json::json!(flexes).to_string());
1305
1306 let group_id = conn.select_row_bound::<_, i64>(sql!(
1307 INSERT INTO pane_groups(
1308 workspace_id,
1309 parent_group_id,
1310 position,
1311 axis,
1312 flexes
1313 )
1314 VALUES (?, ?, ?, ?, ?)
1315 RETURNING group_id
1316 ))?((
1317 workspace_id,
1318 parent_id,
1319 position,
1320 *axis,
1321 flex_string,
1322 ))?
1323 .context("Couldn't retrieve group_id from inserted pane_group")?;
1324
1325 for (position, group) in children.iter().enumerate() {
1326 Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))?
1327 }
1328
1329 Ok(())
1330 }
1331 SerializedPaneGroup::Pane(pane) => {
1332 Self::save_pane(conn, workspace_id, pane, parent)?;
1333 Ok(())
1334 }
1335 }
1336 }
1337
1338 fn save_pane(
1339 conn: &Connection,
1340 workspace_id: WorkspaceId,
1341 pane: &SerializedPane,
1342 parent: Option<(GroupId, usize)>,
1343 ) -> Result<PaneId> {
1344 let pane_id = conn.select_row_bound::<_, i64>(sql!(
1345 INSERT INTO panes(workspace_id, active, pinned_count)
1346 VALUES (?, ?, ?)
1347 RETURNING pane_id
1348 ))?((workspace_id, pane.active, pane.pinned_count))?
1349 .context("Could not retrieve inserted pane_id")?;
1350
1351 let (parent_id, order) = parent.unzip();
1352 conn.exec_bound(sql!(
1353 INSERT INTO center_panes(pane_id, parent_group_id, position)
1354 VALUES (?, ?, ?)
1355 ))?((pane_id, parent_id, order))?;
1356
1357 Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
1358
1359 Ok(pane_id)
1360 }
1361
1362 fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
1363 self.select_bound(sql!(
1364 SELECT kind, item_id, active, preview FROM items
1365 WHERE pane_id = ?
1366 ORDER BY position
1367 ))?(pane_id)
1368 }
1369
1370 fn save_items(
1371 conn: &Connection,
1372 workspace_id: WorkspaceId,
1373 pane_id: PaneId,
1374 items: &[SerializedItem],
1375 ) -> Result<()> {
1376 let mut insert = conn.exec_bound(sql!(
1377 INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active, preview) VALUES (?, ?, ?, ?, ?, ?, ?)
1378 )).context("Preparing insertion")?;
1379 for (position, item) in items.iter().enumerate() {
1380 insert((workspace_id, pane_id, position, item))?;
1381 }
1382
1383 Ok(())
1384 }
1385
1386 query! {
1387 pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> {
1388 UPDATE workspaces
1389 SET timestamp = CURRENT_TIMESTAMP
1390 WHERE workspace_id = ?
1391 }
1392 }
1393
1394 query! {
1395 pub(crate) async fn set_window_open_status(workspace_id: WorkspaceId, bounds: SerializedWindowBounds, display: Uuid) -> Result<()> {
1396 UPDATE workspaces
1397 SET window_state = ?2,
1398 window_x = ?3,
1399 window_y = ?4,
1400 window_width = ?5,
1401 window_height = ?6,
1402 display = ?7
1403 WHERE workspace_id = ?1
1404 }
1405 }
1406
1407 query! {
1408 pub(crate) async fn set_centered_layout(workspace_id: WorkspaceId, centered_layout: bool) -> Result<()> {
1409 UPDATE workspaces
1410 SET centered_layout = ?2
1411 WHERE workspace_id = ?1
1412 }
1413 }
1414
1415 query! {
1416 pub(crate) async fn set_session_id(workspace_id: WorkspaceId, session_id: Option<String>) -> Result<()> {
1417 UPDATE workspaces
1418 SET session_id = ?2
1419 WHERE workspace_id = ?1
1420 }
1421 }
1422
1423 pub async fn toolchain(
1424 &self,
1425 workspace_id: WorkspaceId,
1426 worktree_id: WorktreeId,
1427 relative_path: String,
1428 language_name: LanguageName,
1429 ) -> Result<Option<Toolchain>> {
1430 self.write(move |this| {
1431 let mut select = this
1432 .select_bound(sql!(
1433 SELECT name, path, raw_json FROM toolchains WHERE workspace_id = ? AND language_name = ? AND worktree_id = ? AND relative_path = ?
1434 ))
1435 .context("Preparing insertion")?;
1436
1437 let toolchain: Vec<(String, String, String)> =
1438 select((workspace_id, language_name.as_ref().to_string(), worktree_id.to_usize(), relative_path))?;
1439
1440 Ok(toolchain.into_iter().next().and_then(|(name, path, raw_json)| Some(Toolchain {
1441 name: name.into(),
1442 path: path.into(),
1443 language_name,
1444 as_json: serde_json::Value::from_str(&raw_json).ok()?
1445 })))
1446 })
1447 .await
1448 }
1449
1450 pub(crate) async fn toolchains(
1451 &self,
1452 workspace_id: WorkspaceId,
1453 ) -> Result<Vec<(Toolchain, WorktreeId, Arc<Path>)>> {
1454 self.write(move |this| {
1455 let mut select = this
1456 .select_bound(sql!(
1457 SELECT name, path, worktree_id, relative_worktree_path, language_name, raw_json FROM toolchains WHERE workspace_id = ?
1458 ))
1459 .context("Preparing insertion")?;
1460
1461 let toolchain: Vec<(String, String, u64, String, String, String)> =
1462 select(workspace_id)?;
1463
1464 Ok(toolchain.into_iter().filter_map(|(name, path, worktree_id, relative_worktree_path, language_name, raw_json)| Some((Toolchain {
1465 name: name.into(),
1466 path: path.into(),
1467 language_name: LanguageName::new(&language_name),
1468 as_json: serde_json::Value::from_str(&raw_json).ok()?
1469 }, WorktreeId::from_proto(worktree_id), Arc::from(relative_worktree_path.as_ref())))).collect())
1470 })
1471 .await
1472 }
1473 pub async fn set_toolchain(
1474 &self,
1475 workspace_id: WorkspaceId,
1476 worktree_id: WorktreeId,
1477 relative_worktree_path: String,
1478 toolchain: Toolchain,
1479 ) -> Result<()> {
1480 log::debug!(
1481 "Setting toolchain for workspace, worktree: {worktree_id:?}, relative path: {relative_worktree_path:?}, toolchain: {}",
1482 toolchain.name
1483 );
1484 self.write(move |conn| {
1485 let mut insert = conn
1486 .exec_bound(sql!(
1487 INSERT INTO toolchains(workspace_id, worktree_id, relative_worktree_path, language_name, name, path, raw_json) VALUES (?, ?, ?, ?, ?, ?, ?)
1488 ON CONFLICT DO
1489 UPDATE SET
1490 name = ?5,
1491 path = ?6,
1492 raw_json = ?7
1493 ))
1494 .context("Preparing insertion")?;
1495
1496 insert((
1497 workspace_id,
1498 worktree_id.to_usize(),
1499 relative_worktree_path,
1500 toolchain.language_name.as_ref(),
1501 toolchain.name.as_ref(),
1502 toolchain.path.as_ref(),
1503 toolchain.as_json.to_string(),
1504 ))?;
1505
1506 Ok(())
1507 }).await
1508 }
1509}
1510
1511pub fn delete_unloaded_items(
1512 alive_items: Vec<ItemId>,
1513 workspace_id: WorkspaceId,
1514 table: &'static str,
1515 db: &ThreadSafeConnection,
1516 cx: &mut App,
1517) -> Task<Result<()>> {
1518 let db = db.clone();
1519 cx.spawn(async move |_| {
1520 let placeholders = alive_items
1521 .iter()
1522 .map(|_| "?")
1523 .collect::<Vec<&str>>()
1524 .join(", ");
1525
1526 let query = format!(
1527 "DELETE FROM {table} WHERE workspace_id = ? AND item_id NOT IN ({placeholders})"
1528 );
1529
1530 db.write(move |conn| {
1531 let mut statement = Statement::prepare(conn, query)?;
1532 let mut next_index = statement.bind(&workspace_id, 1)?;
1533 for id in alive_items {
1534 next_index = statement.bind(&id, next_index)?;
1535 }
1536 statement.exec()
1537 })
1538 .await
1539 })
1540}
1541
1542#[cfg(test)]
1543mod tests {
1544 use super::*;
1545 use crate::persistence::model::{
1546 SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
1547 };
1548 use gpui;
1549 use pretty_assertions::assert_eq;
1550 use std::{thread, time::Duration};
1551
1552 #[gpui::test]
1553 async fn test_breakpoints() {
1554 zlog::init_test();
1555
1556 let db = WorkspaceDb::open_test_db("test_breakpoints").await;
1557 let id = db.next_id().await.unwrap();
1558
1559 let path = Path::new("/tmp/test.rs");
1560
1561 let breakpoint = Breakpoint {
1562 position: 123,
1563 message: None,
1564 state: BreakpointState::Enabled,
1565 condition: None,
1566 hit_condition: None,
1567 };
1568
1569 let log_breakpoint = Breakpoint {
1570 position: 456,
1571 message: Some("Test log message".into()),
1572 state: BreakpointState::Enabled,
1573 condition: None,
1574 hit_condition: None,
1575 };
1576
1577 let disable_breakpoint = Breakpoint {
1578 position: 578,
1579 message: None,
1580 state: BreakpointState::Disabled,
1581 condition: None,
1582 hit_condition: None,
1583 };
1584
1585 let condition_breakpoint = Breakpoint {
1586 position: 789,
1587 message: None,
1588 state: BreakpointState::Enabled,
1589 condition: Some("x > 5".into()),
1590 hit_condition: None,
1591 };
1592
1593 let hit_condition_breakpoint = Breakpoint {
1594 position: 999,
1595 message: None,
1596 state: BreakpointState::Enabled,
1597 condition: None,
1598 hit_condition: Some(">= 3".into()),
1599 };
1600
1601 let workspace = SerializedWorkspace {
1602 id,
1603 paths: PathList::new(&["/tmp"]),
1604 location: SerializedWorkspaceLocation::Local,
1605 center_group: Default::default(),
1606 window_bounds: Default::default(),
1607 display: Default::default(),
1608 docks: Default::default(),
1609 centered_layout: false,
1610 breakpoints: {
1611 let mut map = collections::BTreeMap::default();
1612 map.insert(
1613 Arc::from(path),
1614 vec![
1615 SourceBreakpoint {
1616 row: breakpoint.position,
1617 path: Arc::from(path),
1618 message: breakpoint.message.clone(),
1619 state: breakpoint.state,
1620 condition: breakpoint.condition.clone(),
1621 hit_condition: breakpoint.hit_condition.clone(),
1622 },
1623 SourceBreakpoint {
1624 row: log_breakpoint.position,
1625 path: Arc::from(path),
1626 message: log_breakpoint.message.clone(),
1627 state: log_breakpoint.state,
1628 condition: log_breakpoint.condition.clone(),
1629 hit_condition: log_breakpoint.hit_condition.clone(),
1630 },
1631 SourceBreakpoint {
1632 row: disable_breakpoint.position,
1633 path: Arc::from(path),
1634 message: disable_breakpoint.message.clone(),
1635 state: disable_breakpoint.state,
1636 condition: disable_breakpoint.condition.clone(),
1637 hit_condition: disable_breakpoint.hit_condition.clone(),
1638 },
1639 SourceBreakpoint {
1640 row: condition_breakpoint.position,
1641 path: Arc::from(path),
1642 message: condition_breakpoint.message.clone(),
1643 state: condition_breakpoint.state,
1644 condition: condition_breakpoint.condition.clone(),
1645 hit_condition: condition_breakpoint.hit_condition.clone(),
1646 },
1647 SourceBreakpoint {
1648 row: hit_condition_breakpoint.position,
1649 path: Arc::from(path),
1650 message: hit_condition_breakpoint.message.clone(),
1651 state: hit_condition_breakpoint.state,
1652 condition: hit_condition_breakpoint.condition.clone(),
1653 hit_condition: hit_condition_breakpoint.hit_condition.clone(),
1654 },
1655 ],
1656 );
1657 map
1658 },
1659 session_id: None,
1660 window_id: None,
1661 };
1662
1663 db.save_workspace(workspace.clone()).await;
1664
1665 let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
1666 let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(path)).unwrap();
1667
1668 assert_eq!(loaded_breakpoints.len(), 5);
1669
1670 // normal breakpoint
1671 assert_eq!(loaded_breakpoints[0].row, breakpoint.position);
1672 assert_eq!(loaded_breakpoints[0].message, breakpoint.message);
1673 assert_eq!(loaded_breakpoints[0].condition, breakpoint.condition);
1674 assert_eq!(
1675 loaded_breakpoints[0].hit_condition,
1676 breakpoint.hit_condition
1677 );
1678 assert_eq!(loaded_breakpoints[0].state, breakpoint.state);
1679 assert_eq!(loaded_breakpoints[0].path, Arc::from(path));
1680
1681 // enabled breakpoint
1682 assert_eq!(loaded_breakpoints[1].row, log_breakpoint.position);
1683 assert_eq!(loaded_breakpoints[1].message, log_breakpoint.message);
1684 assert_eq!(loaded_breakpoints[1].condition, log_breakpoint.condition);
1685 assert_eq!(
1686 loaded_breakpoints[1].hit_condition,
1687 log_breakpoint.hit_condition
1688 );
1689 assert_eq!(loaded_breakpoints[1].state, log_breakpoint.state);
1690 assert_eq!(loaded_breakpoints[1].path, Arc::from(path));
1691
1692 // disable breakpoint
1693 assert_eq!(loaded_breakpoints[2].row, disable_breakpoint.position);
1694 assert_eq!(loaded_breakpoints[2].message, disable_breakpoint.message);
1695 assert_eq!(
1696 loaded_breakpoints[2].condition,
1697 disable_breakpoint.condition
1698 );
1699 assert_eq!(
1700 loaded_breakpoints[2].hit_condition,
1701 disable_breakpoint.hit_condition
1702 );
1703 assert_eq!(loaded_breakpoints[2].state, disable_breakpoint.state);
1704 assert_eq!(loaded_breakpoints[2].path, Arc::from(path));
1705
1706 // condition breakpoint
1707 assert_eq!(loaded_breakpoints[3].row, condition_breakpoint.position);
1708 assert_eq!(loaded_breakpoints[3].message, condition_breakpoint.message);
1709 assert_eq!(
1710 loaded_breakpoints[3].condition,
1711 condition_breakpoint.condition
1712 );
1713 assert_eq!(
1714 loaded_breakpoints[3].hit_condition,
1715 condition_breakpoint.hit_condition
1716 );
1717 assert_eq!(loaded_breakpoints[3].state, condition_breakpoint.state);
1718 assert_eq!(loaded_breakpoints[3].path, Arc::from(path));
1719
1720 // hit condition breakpoint
1721 assert_eq!(loaded_breakpoints[4].row, hit_condition_breakpoint.position);
1722 assert_eq!(
1723 loaded_breakpoints[4].message,
1724 hit_condition_breakpoint.message
1725 );
1726 assert_eq!(
1727 loaded_breakpoints[4].condition,
1728 hit_condition_breakpoint.condition
1729 );
1730 assert_eq!(
1731 loaded_breakpoints[4].hit_condition,
1732 hit_condition_breakpoint.hit_condition
1733 );
1734 assert_eq!(loaded_breakpoints[4].state, hit_condition_breakpoint.state);
1735 assert_eq!(loaded_breakpoints[4].path, Arc::from(path));
1736 }
1737
1738 #[gpui::test]
1739 async fn test_remove_last_breakpoint() {
1740 zlog::init_test();
1741
1742 let db = WorkspaceDb::open_test_db("test_remove_last_breakpoint").await;
1743 let id = db.next_id().await.unwrap();
1744
1745 let singular_path = Path::new("/tmp/test_remove_last_breakpoint.rs");
1746
1747 let breakpoint_to_remove = Breakpoint {
1748 position: 100,
1749 message: None,
1750 state: BreakpointState::Enabled,
1751 condition: None,
1752 hit_condition: None,
1753 };
1754
1755 let workspace = SerializedWorkspace {
1756 id,
1757 paths: PathList::new(&["/tmp"]),
1758 location: SerializedWorkspaceLocation::Local,
1759 center_group: Default::default(),
1760 window_bounds: Default::default(),
1761 display: Default::default(),
1762 docks: Default::default(),
1763 centered_layout: false,
1764 breakpoints: {
1765 let mut map = collections::BTreeMap::default();
1766 map.insert(
1767 Arc::from(singular_path),
1768 vec![SourceBreakpoint {
1769 row: breakpoint_to_remove.position,
1770 path: Arc::from(singular_path),
1771 message: None,
1772 state: BreakpointState::Enabled,
1773 condition: None,
1774 hit_condition: None,
1775 }],
1776 );
1777 map
1778 },
1779 session_id: None,
1780 window_id: None,
1781 };
1782
1783 db.save_workspace(workspace.clone()).await;
1784
1785 let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
1786 let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(singular_path)).unwrap();
1787
1788 assert_eq!(loaded_breakpoints.len(), 1);
1789 assert_eq!(loaded_breakpoints[0].row, breakpoint_to_remove.position);
1790 assert_eq!(loaded_breakpoints[0].message, breakpoint_to_remove.message);
1791 assert_eq!(
1792 loaded_breakpoints[0].condition,
1793 breakpoint_to_remove.condition
1794 );
1795 assert_eq!(
1796 loaded_breakpoints[0].hit_condition,
1797 breakpoint_to_remove.hit_condition
1798 );
1799 assert_eq!(loaded_breakpoints[0].state, breakpoint_to_remove.state);
1800 assert_eq!(loaded_breakpoints[0].path, Arc::from(singular_path));
1801
1802 let workspace_without_breakpoint = SerializedWorkspace {
1803 id,
1804 paths: PathList::new(&["/tmp"]),
1805 location: SerializedWorkspaceLocation::Local,
1806 center_group: Default::default(),
1807 window_bounds: Default::default(),
1808 display: Default::default(),
1809 docks: Default::default(),
1810 centered_layout: false,
1811 breakpoints: collections::BTreeMap::default(),
1812 session_id: None,
1813 window_id: None,
1814 };
1815
1816 db.save_workspace(workspace_without_breakpoint.clone())
1817 .await;
1818
1819 let loaded_after_remove = db.workspace_for_roots(&["/tmp"]).unwrap();
1820 let empty_breakpoints = loaded_after_remove
1821 .breakpoints
1822 .get(&Arc::from(singular_path));
1823
1824 assert!(empty_breakpoints.is_none());
1825 }
1826
1827 #[gpui::test]
1828 async fn test_next_id_stability() {
1829 zlog::init_test();
1830
1831 let db = WorkspaceDb::open_test_db("test_next_id_stability").await;
1832
1833 db.write(|conn| {
1834 conn.migrate(
1835 "test_table",
1836 &[sql!(
1837 CREATE TABLE test_table(
1838 text TEXT,
1839 workspace_id INTEGER,
1840 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
1841 ON DELETE CASCADE
1842 ) STRICT;
1843 )],
1844 |_, _, _| false,
1845 )
1846 .unwrap();
1847 })
1848 .await;
1849
1850 let id = db.next_id().await.unwrap();
1851 // Assert the empty row got inserted
1852 assert_eq!(
1853 Some(id),
1854 db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
1855 SELECT workspace_id FROM workspaces WHERE workspace_id = ?
1856 ))
1857 .unwrap()(id)
1858 .unwrap()
1859 );
1860
1861 db.write(move |conn| {
1862 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
1863 .unwrap()(("test-text-1", id))
1864 .unwrap()
1865 })
1866 .await;
1867
1868 let test_text_1 = db
1869 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
1870 .unwrap()(1)
1871 .unwrap()
1872 .unwrap();
1873 assert_eq!(test_text_1, "test-text-1");
1874 }
1875
1876 #[gpui::test]
1877 async fn test_workspace_id_stability() {
1878 zlog::init_test();
1879
1880 let db = WorkspaceDb::open_test_db("test_workspace_id_stability").await;
1881
1882 db.write(|conn| {
1883 conn.migrate(
1884 "test_table",
1885 &[sql!(
1886 CREATE TABLE test_table(
1887 text TEXT,
1888 workspace_id INTEGER,
1889 FOREIGN KEY(workspace_id)
1890 REFERENCES workspaces(workspace_id)
1891 ON DELETE CASCADE
1892 ) STRICT;)],
1893 |_, _, _| false,
1894 )
1895 })
1896 .await
1897 .unwrap();
1898
1899 let mut workspace_1 = SerializedWorkspace {
1900 id: WorkspaceId(1),
1901 paths: PathList::new(&["/tmp", "/tmp2"]),
1902 location: SerializedWorkspaceLocation::Local,
1903 center_group: Default::default(),
1904 window_bounds: Default::default(),
1905 display: Default::default(),
1906 docks: Default::default(),
1907 centered_layout: false,
1908 breakpoints: Default::default(),
1909 session_id: None,
1910 window_id: None,
1911 };
1912
1913 let workspace_2 = SerializedWorkspace {
1914 id: WorkspaceId(2),
1915 paths: PathList::new(&["/tmp"]),
1916 location: SerializedWorkspaceLocation::Local,
1917 center_group: Default::default(),
1918 window_bounds: Default::default(),
1919 display: Default::default(),
1920 docks: Default::default(),
1921 centered_layout: false,
1922 breakpoints: Default::default(),
1923 session_id: None,
1924 window_id: None,
1925 };
1926
1927 db.save_workspace(workspace_1.clone()).await;
1928
1929 db.write(|conn| {
1930 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
1931 .unwrap()(("test-text-1", 1))
1932 .unwrap();
1933 })
1934 .await;
1935
1936 db.save_workspace(workspace_2.clone()).await;
1937
1938 db.write(|conn| {
1939 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
1940 .unwrap()(("test-text-2", 2))
1941 .unwrap();
1942 })
1943 .await;
1944
1945 workspace_1.paths = PathList::new(&["/tmp", "/tmp3"]);
1946 db.save_workspace(workspace_1.clone()).await;
1947 db.save_workspace(workspace_1).await;
1948 db.save_workspace(workspace_2).await;
1949
1950 let test_text_2 = db
1951 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
1952 .unwrap()(2)
1953 .unwrap()
1954 .unwrap();
1955 assert_eq!(test_text_2, "test-text-2");
1956
1957 let test_text_1 = db
1958 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
1959 .unwrap()(1)
1960 .unwrap()
1961 .unwrap();
1962 assert_eq!(test_text_1, "test-text-1");
1963 }
1964
1965 fn group(axis: Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
1966 SerializedPaneGroup::Group {
1967 axis: SerializedAxis(axis),
1968 flexes: None,
1969 children,
1970 }
1971 }
1972
1973 #[gpui::test]
1974 async fn test_full_workspace_serialization() {
1975 zlog::init_test();
1976
1977 let db = WorkspaceDb::open_test_db("test_full_workspace_serialization").await;
1978
1979 // -----------------
1980 // | 1,2 | 5,6 |
1981 // | - - - | |
1982 // | 3,4 | |
1983 // -----------------
1984 let center_group = group(
1985 Axis::Horizontal,
1986 vec![
1987 group(
1988 Axis::Vertical,
1989 vec![
1990 SerializedPaneGroup::Pane(SerializedPane::new(
1991 vec![
1992 SerializedItem::new("Terminal", 5, false, false),
1993 SerializedItem::new("Terminal", 6, true, false),
1994 ],
1995 false,
1996 0,
1997 )),
1998 SerializedPaneGroup::Pane(SerializedPane::new(
1999 vec![
2000 SerializedItem::new("Terminal", 7, true, false),
2001 SerializedItem::new("Terminal", 8, false, false),
2002 ],
2003 false,
2004 0,
2005 )),
2006 ],
2007 ),
2008 SerializedPaneGroup::Pane(SerializedPane::new(
2009 vec![
2010 SerializedItem::new("Terminal", 9, false, false),
2011 SerializedItem::new("Terminal", 10, true, false),
2012 ],
2013 false,
2014 0,
2015 )),
2016 ],
2017 );
2018
2019 let workspace = SerializedWorkspace {
2020 id: WorkspaceId(5),
2021 paths: PathList::new(&["/tmp", "/tmp2"]),
2022 location: SerializedWorkspaceLocation::Local,
2023 center_group,
2024 window_bounds: Default::default(),
2025 breakpoints: Default::default(),
2026 display: Default::default(),
2027 docks: Default::default(),
2028 centered_layout: false,
2029 session_id: None,
2030 window_id: Some(999),
2031 };
2032
2033 db.save_workspace(workspace.clone()).await;
2034
2035 let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
2036 assert_eq!(workspace, round_trip_workspace.unwrap());
2037
2038 // Test guaranteed duplicate IDs
2039 db.save_workspace(workspace.clone()).await;
2040 db.save_workspace(workspace.clone()).await;
2041
2042 let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
2043 assert_eq!(workspace, round_trip_workspace.unwrap());
2044 }
2045
2046 #[gpui::test]
2047 async fn test_workspace_assignment() {
2048 zlog::init_test();
2049
2050 let db = WorkspaceDb::open_test_db("test_basic_functionality").await;
2051
2052 let workspace_1 = SerializedWorkspace {
2053 id: WorkspaceId(1),
2054 paths: PathList::new(&["/tmp", "/tmp2"]),
2055 location: SerializedWorkspaceLocation::Local,
2056 center_group: Default::default(),
2057 window_bounds: Default::default(),
2058 breakpoints: Default::default(),
2059 display: Default::default(),
2060 docks: Default::default(),
2061 centered_layout: false,
2062 session_id: None,
2063 window_id: Some(1),
2064 };
2065
2066 let mut workspace_2 = SerializedWorkspace {
2067 id: WorkspaceId(2),
2068 paths: PathList::new(&["/tmp"]),
2069 location: SerializedWorkspaceLocation::Local,
2070 center_group: Default::default(),
2071 window_bounds: Default::default(),
2072 display: Default::default(),
2073 docks: Default::default(),
2074 centered_layout: false,
2075 breakpoints: Default::default(),
2076 session_id: None,
2077 window_id: Some(2),
2078 };
2079
2080 db.save_workspace(workspace_1.clone()).await;
2081 db.save_workspace(workspace_2.clone()).await;
2082
2083 // Test that paths are treated as a set
2084 assert_eq!(
2085 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2086 workspace_1
2087 );
2088 assert_eq!(
2089 db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
2090 workspace_1
2091 );
2092
2093 // Make sure that other keys work
2094 assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
2095 assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
2096
2097 // Test 'mutate' case of updating a pre-existing id
2098 workspace_2.paths = PathList::new(&["/tmp", "/tmp2"]);
2099
2100 db.save_workspace(workspace_2.clone()).await;
2101 assert_eq!(
2102 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2103 workspace_2
2104 );
2105
2106 // Test other mechanism for mutating
2107 let mut workspace_3 = SerializedWorkspace {
2108 id: WorkspaceId(3),
2109 paths: PathList::new(&["/tmp2", "/tmp"]),
2110 location: SerializedWorkspaceLocation::Local,
2111 center_group: Default::default(),
2112 window_bounds: Default::default(),
2113 breakpoints: Default::default(),
2114 display: Default::default(),
2115 docks: Default::default(),
2116 centered_layout: false,
2117 session_id: None,
2118 window_id: Some(3),
2119 };
2120
2121 db.save_workspace(workspace_3.clone()).await;
2122 assert_eq!(
2123 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2124 workspace_3
2125 );
2126
2127 // Make sure that updating paths differently also works
2128 workspace_3.paths = PathList::new(&["/tmp3", "/tmp4", "/tmp2"]);
2129 db.save_workspace(workspace_3.clone()).await;
2130 assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
2131 assert_eq!(
2132 db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
2133 .unwrap(),
2134 workspace_3
2135 );
2136 }
2137
2138 #[gpui::test]
2139 async fn test_session_workspaces() {
2140 zlog::init_test();
2141
2142 let db = WorkspaceDb::open_test_db("test_serializing_workspaces_session_id").await;
2143
2144 let workspace_1 = SerializedWorkspace {
2145 id: WorkspaceId(1),
2146 paths: PathList::new(&["/tmp1"]),
2147 location: SerializedWorkspaceLocation::Local,
2148 center_group: Default::default(),
2149 window_bounds: Default::default(),
2150 display: Default::default(),
2151 docks: Default::default(),
2152 centered_layout: false,
2153 breakpoints: Default::default(),
2154 session_id: Some("session-id-1".to_owned()),
2155 window_id: Some(10),
2156 };
2157
2158 let workspace_2 = SerializedWorkspace {
2159 id: WorkspaceId(2),
2160 paths: PathList::new(&["/tmp2"]),
2161 location: SerializedWorkspaceLocation::Local,
2162 center_group: Default::default(),
2163 window_bounds: Default::default(),
2164 display: Default::default(),
2165 docks: Default::default(),
2166 centered_layout: false,
2167 breakpoints: Default::default(),
2168 session_id: Some("session-id-1".to_owned()),
2169 window_id: Some(20),
2170 };
2171
2172 let workspace_3 = SerializedWorkspace {
2173 id: WorkspaceId(3),
2174 paths: PathList::new(&["/tmp3"]),
2175 location: SerializedWorkspaceLocation::Local,
2176 center_group: Default::default(),
2177 window_bounds: Default::default(),
2178 display: Default::default(),
2179 docks: Default::default(),
2180 centered_layout: false,
2181 breakpoints: Default::default(),
2182 session_id: Some("session-id-2".to_owned()),
2183 window_id: Some(30),
2184 };
2185
2186 let workspace_4 = SerializedWorkspace {
2187 id: WorkspaceId(4),
2188 paths: PathList::new(&["/tmp4"]),
2189 location: SerializedWorkspaceLocation::Local,
2190 center_group: Default::default(),
2191 window_bounds: Default::default(),
2192 display: Default::default(),
2193 docks: Default::default(),
2194 centered_layout: false,
2195 breakpoints: Default::default(),
2196 session_id: None,
2197 window_id: None,
2198 };
2199
2200 let connection_id = db
2201 .get_or_create_ssh_connection("my-host".to_string(), Some(1234), None)
2202 .await
2203 .unwrap();
2204
2205 let workspace_5 = SerializedWorkspace {
2206 id: WorkspaceId(5),
2207 paths: PathList::default(),
2208 location: SerializedWorkspaceLocation::Ssh(db.ssh_connection(connection_id).unwrap()),
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: Some("session-id-2".to_owned()),
2216 window_id: Some(50),
2217 };
2218
2219 let workspace_6 = SerializedWorkspace {
2220 id: WorkspaceId(6),
2221 paths: PathList::new(&["/tmp6a", "/tmp6b", "/tmp6c"]),
2222 location: SerializedWorkspaceLocation::Local,
2223 center_group: Default::default(),
2224 window_bounds: Default::default(),
2225 breakpoints: Default::default(),
2226 display: Default::default(),
2227 docks: Default::default(),
2228 centered_layout: false,
2229 session_id: Some("session-id-3".to_owned()),
2230 window_id: Some(60),
2231 };
2232
2233 db.save_workspace(workspace_1.clone()).await;
2234 thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
2235 db.save_workspace(workspace_2.clone()).await;
2236 db.save_workspace(workspace_3.clone()).await;
2237 thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
2238 db.save_workspace(workspace_4.clone()).await;
2239 db.save_workspace(workspace_5.clone()).await;
2240 db.save_workspace(workspace_6.clone()).await;
2241
2242 let locations = db.session_workspaces("session-id-1".to_owned()).unwrap();
2243 assert_eq!(locations.len(), 2);
2244 assert_eq!(locations[0].0, PathList::new(&["/tmp2"]));
2245 assert_eq!(locations[0].1, Some(20));
2246 assert_eq!(locations[1].0, PathList::new(&["/tmp1"]));
2247 assert_eq!(locations[1].1, Some(10));
2248
2249 let locations = db.session_workspaces("session-id-2".to_owned()).unwrap();
2250 assert_eq!(locations.len(), 2);
2251 assert_eq!(locations[0].0, PathList::default());
2252 assert_eq!(locations[0].1, Some(50));
2253 assert_eq!(locations[0].2, Some(connection_id));
2254 assert_eq!(locations[1].0, PathList::new(&["/tmp3"]));
2255 assert_eq!(locations[1].1, Some(30));
2256
2257 let locations = db.session_workspaces("session-id-3".to_owned()).unwrap();
2258 assert_eq!(locations.len(), 1);
2259 assert_eq!(
2260 locations[0].0,
2261 PathList::new(&["/tmp6a", "/tmp6b", "/tmp6c"]),
2262 );
2263 assert_eq!(locations[0].1, Some(60));
2264 }
2265
2266 fn default_workspace<P: AsRef<Path>>(
2267 paths: &[P],
2268 center_group: &SerializedPaneGroup,
2269 ) -> SerializedWorkspace {
2270 SerializedWorkspace {
2271 id: WorkspaceId(4),
2272 paths: PathList::new(paths),
2273 location: SerializedWorkspaceLocation::Local,
2274 center_group: center_group.clone(),
2275 window_bounds: Default::default(),
2276 display: Default::default(),
2277 docks: Default::default(),
2278 breakpoints: Default::default(),
2279 centered_layout: false,
2280 session_id: None,
2281 window_id: None,
2282 }
2283 }
2284
2285 #[gpui::test]
2286 async fn test_last_session_workspace_locations() {
2287 let dir1 = tempfile::TempDir::with_prefix("dir1").unwrap();
2288 let dir2 = tempfile::TempDir::with_prefix("dir2").unwrap();
2289 let dir3 = tempfile::TempDir::with_prefix("dir3").unwrap();
2290 let dir4 = tempfile::TempDir::with_prefix("dir4").unwrap();
2291
2292 let db =
2293 WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces").await;
2294
2295 let workspaces = [
2296 (1, vec![dir1.path()], 9),
2297 (2, vec![dir2.path()], 5),
2298 (3, vec![dir3.path()], 8),
2299 (4, vec![dir4.path()], 2),
2300 (5, vec![dir1.path(), dir2.path(), dir3.path()], 3),
2301 (6, vec![dir4.path(), dir3.path(), dir2.path()], 4),
2302 ]
2303 .into_iter()
2304 .map(|(id, paths, window_id)| SerializedWorkspace {
2305 id: WorkspaceId(id),
2306 paths: PathList::new(paths.as_slice()),
2307 location: SerializedWorkspaceLocation::Local,
2308 center_group: Default::default(),
2309 window_bounds: Default::default(),
2310 display: Default::default(),
2311 docks: Default::default(),
2312 centered_layout: false,
2313 session_id: Some("one-session".to_owned()),
2314 breakpoints: Default::default(),
2315 window_id: Some(window_id),
2316 })
2317 .collect::<Vec<_>>();
2318
2319 for workspace in workspaces.iter() {
2320 db.save_workspace(workspace.clone()).await;
2321 }
2322
2323 let stack = Some(Vec::from([
2324 WindowId::from(2), // Top
2325 WindowId::from(8),
2326 WindowId::from(5),
2327 WindowId::from(9),
2328 WindowId::from(3),
2329 WindowId::from(4), // Bottom
2330 ]));
2331
2332 let locations = db
2333 .last_session_workspace_locations("one-session", stack)
2334 .unwrap();
2335 assert_eq!(
2336 locations,
2337 [
2338 (
2339 SerializedWorkspaceLocation::Local,
2340 PathList::new(&[dir4.path()])
2341 ),
2342 (
2343 SerializedWorkspaceLocation::Local,
2344 PathList::new(&[dir3.path()])
2345 ),
2346 (
2347 SerializedWorkspaceLocation::Local,
2348 PathList::new(&[dir2.path()])
2349 ),
2350 (
2351 SerializedWorkspaceLocation::Local,
2352 PathList::new(&[dir1.path()])
2353 ),
2354 (
2355 SerializedWorkspaceLocation::Local,
2356 PathList::new(&[dir1.path(), dir2.path(), dir3.path()])
2357 ),
2358 (
2359 SerializedWorkspaceLocation::Local,
2360 PathList::new(&[dir4.path(), dir3.path(), dir2.path()])
2361 ),
2362 ]
2363 );
2364 }
2365
2366 #[gpui::test]
2367 async fn test_last_session_workspace_locations_ssh_projects() {
2368 let db = WorkspaceDb::open_test_db(
2369 "test_serializing_workspaces_last_session_workspaces_ssh_projects",
2370 )
2371 .await;
2372
2373 let ssh_connections = [
2374 ("host-1", "my-user-1"),
2375 ("host-2", "my-user-2"),
2376 ("host-3", "my-user-3"),
2377 ("host-4", "my-user-4"),
2378 ]
2379 .into_iter()
2380 .map(|(host, user)| async {
2381 db.get_or_create_ssh_connection(host.to_string(), None, Some(user.to_string()))
2382 .await
2383 .unwrap();
2384 SerializedSshConnection {
2385 host: host.into(),
2386 port: None,
2387 user: Some(user.into()),
2388 }
2389 })
2390 .collect::<Vec<_>>();
2391
2392 let ssh_connections = futures::future::join_all(ssh_connections).await;
2393
2394 let workspaces = [
2395 (1, ssh_connections[0].clone(), 9),
2396 (2, ssh_connections[1].clone(), 5),
2397 (3, ssh_connections[2].clone(), 8),
2398 (4, ssh_connections[3].clone(), 2),
2399 ]
2400 .into_iter()
2401 .map(|(id, ssh_connection, window_id)| SerializedWorkspace {
2402 id: WorkspaceId(id),
2403 paths: PathList::default(),
2404 location: SerializedWorkspaceLocation::Ssh(ssh_connection),
2405 center_group: Default::default(),
2406 window_bounds: Default::default(),
2407 display: Default::default(),
2408 docks: Default::default(),
2409 centered_layout: false,
2410 session_id: Some("one-session".to_owned()),
2411 breakpoints: Default::default(),
2412 window_id: Some(window_id),
2413 })
2414 .collect::<Vec<_>>();
2415
2416 for workspace in workspaces.iter() {
2417 db.save_workspace(workspace.clone()).await;
2418 }
2419
2420 let stack = Some(Vec::from([
2421 WindowId::from(2), // Top
2422 WindowId::from(8),
2423 WindowId::from(5),
2424 WindowId::from(9), // Bottom
2425 ]));
2426
2427 let have = db
2428 .last_session_workspace_locations("one-session", stack)
2429 .unwrap();
2430 assert_eq!(have.len(), 4);
2431 assert_eq!(
2432 have[0],
2433 (
2434 SerializedWorkspaceLocation::Ssh(ssh_connections[3].clone()),
2435 PathList::default()
2436 )
2437 );
2438 assert_eq!(
2439 have[1],
2440 (
2441 SerializedWorkspaceLocation::Ssh(ssh_connections[2].clone()),
2442 PathList::default()
2443 )
2444 );
2445 assert_eq!(
2446 have[2],
2447 (
2448 SerializedWorkspaceLocation::Ssh(ssh_connections[1].clone()),
2449 PathList::default()
2450 )
2451 );
2452 assert_eq!(
2453 have[3],
2454 (
2455 SerializedWorkspaceLocation::Ssh(ssh_connections[0].clone()),
2456 PathList::default()
2457 )
2458 );
2459 }
2460
2461 #[gpui::test]
2462 async fn test_get_or_create_ssh_project() {
2463 let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project").await;
2464
2465 let host = "example.com".to_string();
2466 let port = Some(22_u16);
2467 let user = Some("user".to_string());
2468
2469 let connection_id = db
2470 .get_or_create_ssh_connection(host.clone(), port, user.clone())
2471 .await
2472 .unwrap();
2473
2474 // Test that calling the function again with the same parameters returns the same project
2475 let same_connection = db
2476 .get_or_create_ssh_connection(host.clone(), port, user.clone())
2477 .await
2478 .unwrap();
2479
2480 assert_eq!(connection_id, same_connection);
2481
2482 // Test with different parameters
2483 let host2 = "otherexample.com".to_string();
2484 let port2 = None;
2485 let user2 = Some("otheruser".to_string());
2486
2487 let different_connection = db
2488 .get_or_create_ssh_connection(host2.clone(), port2, user2.clone())
2489 .await
2490 .unwrap();
2491
2492 assert_ne!(connection_id, different_connection);
2493 }
2494
2495 #[gpui::test]
2496 async fn test_get_or_create_ssh_project_with_null_user() {
2497 let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project_with_null_user").await;
2498
2499 let (host, port, user) = ("example.com".to_string(), None, None);
2500
2501 let connection_id = db
2502 .get_or_create_ssh_connection(host.clone(), port, None)
2503 .await
2504 .unwrap();
2505
2506 let same_connection_id = db
2507 .get_or_create_ssh_connection(host.clone(), port, user.clone())
2508 .await
2509 .unwrap();
2510
2511 assert_eq!(connection_id, same_connection_id);
2512 }
2513
2514 #[gpui::test]
2515 async fn test_get_ssh_connections() {
2516 let db = WorkspaceDb::open_test_db("test_get_ssh_connections").await;
2517
2518 let connections = [
2519 ("example.com".to_string(), None, None),
2520 (
2521 "anotherexample.com".to_string(),
2522 Some(123_u16),
2523 Some("user2".to_string()),
2524 ),
2525 ("yetanother.com".to_string(), Some(345_u16), None),
2526 ];
2527
2528 let mut ids = Vec::new();
2529 for (host, port, user) in connections.iter() {
2530 ids.push(
2531 db.get_or_create_ssh_connection(host.clone(), *port, user.clone())
2532 .await
2533 .unwrap(),
2534 );
2535 }
2536
2537 let stored_projects = db.ssh_connections().unwrap();
2538 assert_eq!(
2539 stored_projects,
2540 [
2541 (
2542 ids[0],
2543 SerializedSshConnection {
2544 host: "example.com".into(),
2545 port: None,
2546 user: None,
2547 }
2548 ),
2549 (
2550 ids[1],
2551 SerializedSshConnection {
2552 host: "anotherexample.com".into(),
2553 port: Some(123),
2554 user: Some("user2".into()),
2555 }
2556 ),
2557 (
2558 ids[2],
2559 SerializedSshConnection {
2560 host: "yetanother.com".into(),
2561 port: Some(345),
2562 user: None,
2563 }
2564 ),
2565 ]
2566 .into_iter()
2567 .collect::<HashMap<_, _>>(),
2568 );
2569 }
2570
2571 #[gpui::test]
2572 async fn test_simple_split() {
2573 zlog::init_test();
2574
2575 let db = WorkspaceDb::open_test_db("simple_split").await;
2576
2577 // -----------------
2578 // | 1,2 | 5,6 |
2579 // | - - - | |
2580 // | 3,4 | |
2581 // -----------------
2582 let center_pane = group(
2583 Axis::Horizontal,
2584 vec![
2585 group(
2586 Axis::Vertical,
2587 vec![
2588 SerializedPaneGroup::Pane(SerializedPane::new(
2589 vec![
2590 SerializedItem::new("Terminal", 1, false, false),
2591 SerializedItem::new("Terminal", 2, true, false),
2592 ],
2593 false,
2594 0,
2595 )),
2596 SerializedPaneGroup::Pane(SerializedPane::new(
2597 vec![
2598 SerializedItem::new("Terminal", 4, false, false),
2599 SerializedItem::new("Terminal", 3, true, false),
2600 ],
2601 true,
2602 0,
2603 )),
2604 ],
2605 ),
2606 SerializedPaneGroup::Pane(SerializedPane::new(
2607 vec![
2608 SerializedItem::new("Terminal", 5, true, false),
2609 SerializedItem::new("Terminal", 6, false, false),
2610 ],
2611 false,
2612 0,
2613 )),
2614 ],
2615 );
2616
2617 let workspace = default_workspace(&["/tmp"], ¢er_pane);
2618
2619 db.save_workspace(workspace.clone()).await;
2620
2621 let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
2622
2623 assert_eq!(workspace.center_group, new_workspace.center_group);
2624 }
2625
2626 #[gpui::test]
2627 async fn test_cleanup_panes() {
2628 zlog::init_test();
2629
2630 let db = WorkspaceDb::open_test_db("test_cleanup_panes").await;
2631
2632 let center_pane = group(
2633 Axis::Horizontal,
2634 vec![
2635 group(
2636 Axis::Vertical,
2637 vec![
2638 SerializedPaneGroup::Pane(SerializedPane::new(
2639 vec![
2640 SerializedItem::new("Terminal", 1, false, false),
2641 SerializedItem::new("Terminal", 2, true, false),
2642 ],
2643 false,
2644 0,
2645 )),
2646 SerializedPaneGroup::Pane(SerializedPane::new(
2647 vec![
2648 SerializedItem::new("Terminal", 4, false, false),
2649 SerializedItem::new("Terminal", 3, true, false),
2650 ],
2651 true,
2652 0,
2653 )),
2654 ],
2655 ),
2656 SerializedPaneGroup::Pane(SerializedPane::new(
2657 vec![
2658 SerializedItem::new("Terminal", 5, false, false),
2659 SerializedItem::new("Terminal", 6, true, false),
2660 ],
2661 false,
2662 0,
2663 )),
2664 ],
2665 );
2666
2667 let id = &["/tmp"];
2668
2669 let mut workspace = default_workspace(id, ¢er_pane);
2670
2671 db.save_workspace(workspace.clone()).await;
2672
2673 workspace.center_group = group(
2674 Axis::Vertical,
2675 vec![
2676 SerializedPaneGroup::Pane(SerializedPane::new(
2677 vec![
2678 SerializedItem::new("Terminal", 1, false, false),
2679 SerializedItem::new("Terminal", 2, true, false),
2680 ],
2681 false,
2682 0,
2683 )),
2684 SerializedPaneGroup::Pane(SerializedPane::new(
2685 vec![
2686 SerializedItem::new("Terminal", 4, true, false),
2687 SerializedItem::new("Terminal", 3, false, false),
2688 ],
2689 true,
2690 0,
2691 )),
2692 ],
2693 );
2694
2695 db.save_workspace(workspace.clone()).await;
2696
2697 let new_workspace = db.workspace_for_roots(id).unwrap();
2698
2699 assert_eq!(workspace.center_group, new_workspace.center_group);
2700 }
2701}