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