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