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