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