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