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