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