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