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 next_id() -> Result<WorkspaceId> {
944 INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id
945 }
946 }
947
948 query! {
949 fn recent_workspaces() -> Result<Vec<(WorkspaceId, LocalPaths, LocalPathsOrder, Option<u64>)>> {
950 SELECT workspace_id, local_paths, local_paths_order, ssh_project_id
951 FROM workspaces
952 WHERE local_paths IS NOT NULL
953 OR ssh_project_id IS NOT NULL
954 ORDER BY timestamp DESC
955 }
956 }
957
958 query! {
959 fn session_workspaces(session_id: String) -> Result<Vec<(LocalPaths, LocalPathsOrder, Option<u64>, Option<u64>)>> {
960 SELECT local_paths, local_paths_order, window_id, ssh_project_id
961 FROM workspaces
962 WHERE session_id = ?1 AND dev_server_project_id IS NULL
963 ORDER BY timestamp DESC
964 }
965 }
966
967 query! {
968 pub fn breakpoints_for_file(workspace_id: WorkspaceId, file_path: &Path) -> Result<Vec<Breakpoint>> {
969 SELECT breakpoint_location
970 FROM breakpoints
971 WHERE workspace_id= ?1 AND path = ?2
972 }
973 }
974
975 query! {
976 pub fn clear_breakpoints(file_path: &Path) -> Result<()> {
977 DELETE FROM breakpoints
978 WHERE file_path = ?2
979 }
980 }
981
982 query! {
983 fn ssh_projects() -> Result<Vec<SerializedSshProject>> {
984 SELECT id, host, port, paths, user
985 FROM ssh_projects
986 }
987 }
988
989 query! {
990 fn ssh_project(id: u64) -> Result<SerializedSshProject> {
991 SELECT id, host, port, paths, user
992 FROM ssh_projects
993 WHERE id = ?
994 }
995 }
996
997 pub(crate) fn last_window(
998 &self,
999 ) -> anyhow::Result<(Option<Uuid>, Option<SerializedWindowBounds>)> {
1000 let mut prepared_query =
1001 self.select::<(Option<Uuid>, Option<SerializedWindowBounds>)>(sql!(
1002 SELECT
1003 display,
1004 window_state, window_x, window_y, window_width, window_height
1005 FROM workspaces
1006 WHERE local_paths
1007 IS NOT NULL
1008 ORDER BY timestamp DESC
1009 LIMIT 1
1010 ))?;
1011 let result = prepared_query()?;
1012 Ok(result.into_iter().next().unwrap_or((None, None)))
1013 }
1014
1015 query! {
1016 pub async fn delete_workspace_by_id(id: WorkspaceId) -> Result<()> {
1017 DELETE FROM toolchains WHERE workspace_id = ?1;
1018 DELETE FROM workspaces
1019 WHERE workspace_id IS ?
1020 }
1021 }
1022
1023 pub async fn delete_workspace_by_dev_server_project_id(
1024 &self,
1025 id: DevServerProjectId,
1026 ) -> Result<()> {
1027 self.write(move |conn| {
1028 conn.exec_bound(sql!(
1029 DELETE FROM dev_server_projects WHERE id = ?
1030 ))?(id.0)?;
1031 conn.exec_bound(sql!(
1032 DELETE FROM toolchains WHERE workspace_id = ?1;
1033 DELETE FROM workspaces
1034 WHERE dev_server_project_id IS ?
1035 ))?(id.0)
1036 })
1037 .await
1038 }
1039
1040 // Returns the recent locations which are still valid on disk and deletes ones which no longer
1041 // exist.
1042 pub async fn recent_workspaces_on_disk(
1043 &self,
1044 ) -> Result<Vec<(WorkspaceId, SerializedWorkspaceLocation)>> {
1045 let mut result = Vec::new();
1046 let mut delete_tasks = Vec::new();
1047 let ssh_projects = self.ssh_projects()?;
1048
1049 for (id, location, order, ssh_project_id) in self.recent_workspaces()? {
1050 if let Some(ssh_project_id) = ssh_project_id.map(SshProjectId) {
1051 if let Some(ssh_project) = ssh_projects.iter().find(|rp| rp.id == ssh_project_id) {
1052 result.push((id, SerializedWorkspaceLocation::Ssh(ssh_project.clone())));
1053 } else {
1054 delete_tasks.push(self.delete_workspace_by_id(id));
1055 }
1056 continue;
1057 }
1058
1059 if location.paths().iter().all(|path| path.exists())
1060 && location.paths().iter().any(|path| path.is_dir())
1061 {
1062 result.push((id, SerializedWorkspaceLocation::Local(location, order)));
1063 } else {
1064 delete_tasks.push(self.delete_workspace_by_id(id));
1065 }
1066 }
1067
1068 futures::future::join_all(delete_tasks).await;
1069 Ok(result)
1070 }
1071
1072 pub async fn last_workspace(&self) -> Result<Option<SerializedWorkspaceLocation>> {
1073 Ok(self
1074 .recent_workspaces_on_disk()
1075 .await?
1076 .into_iter()
1077 .next()
1078 .map(|(_, location)| location))
1079 }
1080
1081 // Returns the locations of the workspaces that were still opened when the last
1082 // session was closed (i.e. when Zed was quit).
1083 // If `last_session_window_order` is provided, the returned locations are ordered
1084 // according to that.
1085 pub fn last_session_workspace_locations(
1086 &self,
1087 last_session_id: &str,
1088 last_session_window_stack: Option<Vec<WindowId>>,
1089 ) -> Result<Vec<SerializedWorkspaceLocation>> {
1090 let mut workspaces = Vec::new();
1091
1092 for (location, order, window_id, ssh_project_id) in
1093 self.session_workspaces(last_session_id.to_owned())?
1094 {
1095 if let Some(ssh_project_id) = ssh_project_id {
1096 let location = SerializedWorkspaceLocation::Ssh(self.ssh_project(ssh_project_id)?);
1097 workspaces.push((location, window_id.map(WindowId::from)));
1098 } else if location.paths().iter().all(|path| path.exists())
1099 && location.paths().iter().any(|path| path.is_dir())
1100 {
1101 let location = SerializedWorkspaceLocation::Local(location, order);
1102 workspaces.push((location, window_id.map(WindowId::from)));
1103 }
1104 }
1105
1106 if let Some(stack) = last_session_window_stack {
1107 workspaces.sort_by_key(|(_, window_id)| {
1108 window_id
1109 .and_then(|id| stack.iter().position(|&order_id| order_id == id))
1110 .unwrap_or(usize::MAX)
1111 });
1112 }
1113
1114 Ok(workspaces
1115 .into_iter()
1116 .map(|(paths, _)| paths)
1117 .collect::<Vec<_>>())
1118 }
1119
1120 fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
1121 Ok(self
1122 .get_pane_group(workspace_id, None)?
1123 .into_iter()
1124 .next()
1125 .unwrap_or_else(|| {
1126 SerializedPaneGroup::Pane(SerializedPane {
1127 active: true,
1128 children: vec![],
1129 pinned_count: 0,
1130 })
1131 }))
1132 }
1133
1134 fn get_pane_group(
1135 &self,
1136 workspace_id: WorkspaceId,
1137 group_id: Option<GroupId>,
1138 ) -> Result<Vec<SerializedPaneGroup>> {
1139 type GroupKey = (Option<GroupId>, WorkspaceId);
1140 type GroupOrPane = (
1141 Option<GroupId>,
1142 Option<SerializedAxis>,
1143 Option<PaneId>,
1144 Option<bool>,
1145 Option<usize>,
1146 Option<String>,
1147 );
1148 self.select_bound::<GroupKey, GroupOrPane>(sql!(
1149 SELECT group_id, axis, pane_id, active, pinned_count, flexes
1150 FROM (SELECT
1151 group_id,
1152 axis,
1153 NULL as pane_id,
1154 NULL as active,
1155 NULL as pinned_count,
1156 position,
1157 parent_group_id,
1158 workspace_id,
1159 flexes
1160 FROM pane_groups
1161 UNION
1162 SELECT
1163 NULL,
1164 NULL,
1165 center_panes.pane_id,
1166 panes.active as active,
1167 pinned_count,
1168 position,
1169 parent_group_id,
1170 panes.workspace_id as workspace_id,
1171 NULL
1172 FROM center_panes
1173 JOIN panes ON center_panes.pane_id = panes.pane_id)
1174 WHERE parent_group_id IS ? AND workspace_id = ?
1175 ORDER BY position
1176 ))?((group_id, workspace_id))?
1177 .into_iter()
1178 .map(|(group_id, axis, pane_id, active, pinned_count, flexes)| {
1179 let maybe_pane = maybe!({ Some((pane_id?, active?, pinned_count?)) });
1180 if let Some((group_id, axis)) = group_id.zip(axis) {
1181 let flexes = flexes
1182 .map(|flexes: String| serde_json::from_str::<Vec<f32>>(&flexes))
1183 .transpose()?;
1184
1185 Ok(SerializedPaneGroup::Group {
1186 axis,
1187 children: self.get_pane_group(workspace_id, Some(group_id))?,
1188 flexes,
1189 })
1190 } else if let Some((pane_id, active, pinned_count)) = maybe_pane {
1191 Ok(SerializedPaneGroup::Pane(SerializedPane::new(
1192 self.get_items(pane_id)?,
1193 active,
1194 pinned_count,
1195 )))
1196 } else {
1197 bail!("Pane Group Child was neither a pane group or a pane");
1198 }
1199 })
1200 // Filter out panes and pane groups which don't have any children or items
1201 .filter(|pane_group| match pane_group {
1202 Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
1203 Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
1204 _ => true,
1205 })
1206 .collect::<Result<_>>()
1207 }
1208
1209 fn save_pane_group(
1210 conn: &Connection,
1211 workspace_id: WorkspaceId,
1212 pane_group: &SerializedPaneGroup,
1213 parent: Option<(GroupId, usize)>,
1214 ) -> Result<()> {
1215 if parent.is_none() {
1216 log::debug!("Saving a pane group for workspace {workspace_id:?}");
1217 }
1218 match pane_group {
1219 SerializedPaneGroup::Group {
1220 axis,
1221 children,
1222 flexes,
1223 } => {
1224 let (parent_id, position) = parent.unzip();
1225
1226 let flex_string = flexes
1227 .as_ref()
1228 .map(|flexes| serde_json::json!(flexes).to_string());
1229
1230 let group_id = conn.select_row_bound::<_, i64>(sql!(
1231 INSERT INTO pane_groups(
1232 workspace_id,
1233 parent_group_id,
1234 position,
1235 axis,
1236 flexes
1237 )
1238 VALUES (?, ?, ?, ?, ?)
1239 RETURNING group_id
1240 ))?((
1241 workspace_id,
1242 parent_id,
1243 position,
1244 *axis,
1245 flex_string,
1246 ))?
1247 .context("Couldn't retrieve group_id from inserted pane_group")?;
1248
1249 for (position, group) in children.iter().enumerate() {
1250 Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))?
1251 }
1252
1253 Ok(())
1254 }
1255 SerializedPaneGroup::Pane(pane) => {
1256 Self::save_pane(conn, workspace_id, pane, parent)?;
1257 Ok(())
1258 }
1259 }
1260 }
1261
1262 fn save_pane(
1263 conn: &Connection,
1264 workspace_id: WorkspaceId,
1265 pane: &SerializedPane,
1266 parent: Option<(GroupId, usize)>,
1267 ) -> Result<PaneId> {
1268 let pane_id = conn.select_row_bound::<_, i64>(sql!(
1269 INSERT INTO panes(workspace_id, active, pinned_count)
1270 VALUES (?, ?, ?)
1271 RETURNING pane_id
1272 ))?((workspace_id, pane.active, pane.pinned_count))?
1273 .context("Could not retrieve inserted pane_id")?;
1274
1275 let (parent_id, order) = parent.unzip();
1276 conn.exec_bound(sql!(
1277 INSERT INTO center_panes(pane_id, parent_group_id, position)
1278 VALUES (?, ?, ?)
1279 ))?((pane_id, parent_id, order))?;
1280
1281 Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
1282
1283 Ok(pane_id)
1284 }
1285
1286 fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
1287 self.select_bound(sql!(
1288 SELECT kind, item_id, active, preview FROM items
1289 WHERE pane_id = ?
1290 ORDER BY position
1291 ))?(pane_id)
1292 }
1293
1294 fn save_items(
1295 conn: &Connection,
1296 workspace_id: WorkspaceId,
1297 pane_id: PaneId,
1298 items: &[SerializedItem],
1299 ) -> Result<()> {
1300 let mut insert = conn.exec_bound(sql!(
1301 INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active, preview) VALUES (?, ?, ?, ?, ?, ?, ?)
1302 )).context("Preparing insertion")?;
1303 for (position, item) in items.iter().enumerate() {
1304 insert((workspace_id, pane_id, position, item))?;
1305 }
1306
1307 Ok(())
1308 }
1309
1310 query! {
1311 pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> {
1312 UPDATE workspaces
1313 SET timestamp = CURRENT_TIMESTAMP
1314 WHERE workspace_id = ?
1315 }
1316 }
1317
1318 query! {
1319 pub(crate) async fn set_window_open_status(workspace_id: WorkspaceId, bounds: SerializedWindowBounds, display: Uuid) -> Result<()> {
1320 UPDATE workspaces
1321 SET window_state = ?2,
1322 window_x = ?3,
1323 window_y = ?4,
1324 window_width = ?5,
1325 window_height = ?6,
1326 display = ?7
1327 WHERE workspace_id = ?1
1328 }
1329 }
1330
1331 query! {
1332 pub(crate) async fn set_centered_layout(workspace_id: WorkspaceId, centered_layout: bool) -> Result<()> {
1333 UPDATE workspaces
1334 SET centered_layout = ?2
1335 WHERE workspace_id = ?1
1336 }
1337 }
1338
1339 query! {
1340 pub(crate) async fn set_session_id(workspace_id: WorkspaceId, session_id: Option<String>) -> Result<()> {
1341 UPDATE workspaces
1342 SET session_id = ?2
1343 WHERE workspace_id = ?1
1344 }
1345 }
1346
1347 pub async fn toolchain(
1348 &self,
1349 workspace_id: WorkspaceId,
1350 worktree_id: WorktreeId,
1351 relative_path: String,
1352 language_name: LanguageName,
1353 ) -> Result<Option<Toolchain>> {
1354 self.write(move |this| {
1355 let mut select = this
1356 .select_bound(sql!(
1357 SELECT name, path, raw_json FROM toolchains WHERE workspace_id = ? AND language_name = ? AND worktree_id = ? AND relative_path = ?
1358 ))
1359 .context("Preparing insertion")?;
1360
1361 let toolchain: Vec<(String, String, String)> =
1362 select((workspace_id, language_name.as_ref().to_string(), worktree_id.to_usize(), relative_path))?;
1363
1364 Ok(toolchain.into_iter().next().and_then(|(name, path, raw_json)| Some(Toolchain {
1365 name: name.into(),
1366 path: path.into(),
1367 language_name,
1368 as_json: serde_json::Value::from_str(&raw_json).ok()?
1369 })))
1370 })
1371 .await
1372 }
1373
1374 pub(crate) async fn toolchains(
1375 &self,
1376 workspace_id: WorkspaceId,
1377 ) -> Result<Vec<(Toolchain, WorktreeId, Arc<Path>)>> {
1378 self.write(move |this| {
1379 let mut select = this
1380 .select_bound(sql!(
1381 SELECT name, path, worktree_id, relative_worktree_path, language_name, raw_json FROM toolchains WHERE workspace_id = ?
1382 ))
1383 .context("Preparing insertion")?;
1384
1385 let toolchain: Vec<(String, String, u64, String, String, String)> =
1386 select(workspace_id)?;
1387
1388 Ok(toolchain.into_iter().filter_map(|(name, path, worktree_id, relative_worktree_path, language_name, raw_json)| Some((Toolchain {
1389 name: name.into(),
1390 path: path.into(),
1391 language_name: LanguageName::new(&language_name),
1392 as_json: serde_json::Value::from_str(&raw_json).ok()?
1393 }, WorktreeId::from_proto(worktree_id), Arc::from(relative_worktree_path.as_ref())))).collect())
1394 })
1395 .await
1396 }
1397 pub async fn set_toolchain(
1398 &self,
1399 workspace_id: WorkspaceId,
1400 worktree_id: WorktreeId,
1401 relative_worktree_path: String,
1402 toolchain: Toolchain,
1403 ) -> Result<()> {
1404 log::debug!(
1405 "Setting toolchain for workspace, worktree: {worktree_id:?}, relative path: {relative_worktree_path:?}, toolchain: {}",
1406 toolchain.name
1407 );
1408 self.write(move |conn| {
1409 let mut insert = conn
1410 .exec_bound(sql!(
1411 INSERT INTO toolchains(workspace_id, worktree_id, relative_worktree_path, language_name, name, path) VALUES (?, ?, ?, ?, ?, ?)
1412 ON CONFLICT DO
1413 UPDATE SET
1414 name = ?5,
1415 path = ?6
1416
1417 ))
1418 .context("Preparing insertion")?;
1419
1420 insert((
1421 workspace_id,
1422 worktree_id.to_usize(),
1423 relative_worktree_path,
1424 toolchain.language_name.as_ref(),
1425 toolchain.name.as_ref(),
1426 toolchain.path.as_ref(),
1427 ))?;
1428
1429 Ok(())
1430 }).await
1431 }
1432}
1433
1434pub fn delete_unloaded_items(
1435 alive_items: Vec<ItemId>,
1436 workspace_id: WorkspaceId,
1437 table: &'static str,
1438 db: &ThreadSafeConnection,
1439 cx: &mut App,
1440) -> Task<Result<()>> {
1441 let db = db.clone();
1442 cx.spawn(async move |_| {
1443 let placeholders = alive_items
1444 .iter()
1445 .map(|_| "?")
1446 .collect::<Vec<&str>>()
1447 .join(", ");
1448
1449 let query = format!(
1450 "DELETE FROM {table} WHERE workspace_id = ? AND item_id NOT IN ({placeholders})"
1451 );
1452
1453 db.write(move |conn| {
1454 let mut statement = Statement::prepare(conn, query)?;
1455 let mut next_index = statement.bind(&workspace_id, 1)?;
1456 for id in alive_items {
1457 next_index = statement.bind(&id, next_index)?;
1458 }
1459 statement.exec()
1460 })
1461 .await
1462 })
1463}
1464
1465#[cfg(test)]
1466mod tests {
1467 use std::thread;
1468 use std::time::Duration;
1469
1470 use super::*;
1471 use crate::persistence::model::SerializedWorkspace;
1472 use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
1473 use gpui;
1474
1475 #[gpui::test]
1476 async fn test_breakpoints() {
1477 zlog::init_test();
1478
1479 let db = WorkspaceDb::open_test_db("test_breakpoints").await;
1480 let id = db.next_id().await.unwrap();
1481
1482 let path = Path::new("/tmp/test.rs");
1483
1484 let breakpoint = Breakpoint {
1485 position: 123,
1486 message: None,
1487 state: BreakpointState::Enabled,
1488 condition: None,
1489 hit_condition: None,
1490 };
1491
1492 let log_breakpoint = Breakpoint {
1493 position: 456,
1494 message: Some("Test log message".into()),
1495 state: BreakpointState::Enabled,
1496 condition: None,
1497 hit_condition: None,
1498 };
1499
1500 let disable_breakpoint = Breakpoint {
1501 position: 578,
1502 message: None,
1503 state: BreakpointState::Disabled,
1504 condition: None,
1505 hit_condition: None,
1506 };
1507
1508 let condition_breakpoint = Breakpoint {
1509 position: 789,
1510 message: None,
1511 state: BreakpointState::Enabled,
1512 condition: Some("x > 5".into()),
1513 hit_condition: None,
1514 };
1515
1516 let hit_condition_breakpoint = Breakpoint {
1517 position: 999,
1518 message: None,
1519 state: BreakpointState::Enabled,
1520 condition: None,
1521 hit_condition: Some(">= 3".into()),
1522 };
1523
1524 let workspace = SerializedWorkspace {
1525 id,
1526 location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]),
1527 center_group: Default::default(),
1528 window_bounds: Default::default(),
1529 display: Default::default(),
1530 docks: Default::default(),
1531 centered_layout: false,
1532 breakpoints: {
1533 let mut map = collections::BTreeMap::default();
1534 map.insert(
1535 Arc::from(path),
1536 vec![
1537 SourceBreakpoint {
1538 row: breakpoint.position,
1539 path: Arc::from(path),
1540 message: breakpoint.message.clone(),
1541 state: breakpoint.state,
1542 condition: breakpoint.condition.clone(),
1543 hit_condition: breakpoint.hit_condition.clone(),
1544 },
1545 SourceBreakpoint {
1546 row: log_breakpoint.position,
1547 path: Arc::from(path),
1548 message: log_breakpoint.message.clone(),
1549 state: log_breakpoint.state,
1550 condition: log_breakpoint.condition.clone(),
1551 hit_condition: log_breakpoint.hit_condition.clone(),
1552 },
1553 SourceBreakpoint {
1554 row: disable_breakpoint.position,
1555 path: Arc::from(path),
1556 message: disable_breakpoint.message.clone(),
1557 state: disable_breakpoint.state,
1558 condition: disable_breakpoint.condition.clone(),
1559 hit_condition: disable_breakpoint.hit_condition.clone(),
1560 },
1561 SourceBreakpoint {
1562 row: condition_breakpoint.position,
1563 path: Arc::from(path),
1564 message: condition_breakpoint.message.clone(),
1565 state: condition_breakpoint.state,
1566 condition: condition_breakpoint.condition.clone(),
1567 hit_condition: condition_breakpoint.hit_condition.clone(),
1568 },
1569 SourceBreakpoint {
1570 row: hit_condition_breakpoint.position,
1571 path: Arc::from(path),
1572 message: hit_condition_breakpoint.message.clone(),
1573 state: hit_condition_breakpoint.state,
1574 condition: hit_condition_breakpoint.condition.clone(),
1575 hit_condition: hit_condition_breakpoint.hit_condition.clone(),
1576 },
1577 ],
1578 );
1579 map
1580 },
1581 session_id: None,
1582 window_id: None,
1583 };
1584
1585 db.save_workspace(workspace.clone()).await;
1586
1587 let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
1588 let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(path)).unwrap();
1589
1590 assert_eq!(loaded_breakpoints.len(), 5);
1591
1592 // normal breakpoint
1593 assert_eq!(loaded_breakpoints[0].row, breakpoint.position);
1594 assert_eq!(loaded_breakpoints[0].message, breakpoint.message);
1595 assert_eq!(loaded_breakpoints[0].condition, breakpoint.condition);
1596 assert_eq!(
1597 loaded_breakpoints[0].hit_condition,
1598 breakpoint.hit_condition
1599 );
1600 assert_eq!(loaded_breakpoints[0].state, breakpoint.state);
1601 assert_eq!(loaded_breakpoints[0].path, Arc::from(path));
1602
1603 // enabled breakpoint
1604 assert_eq!(loaded_breakpoints[1].row, log_breakpoint.position);
1605 assert_eq!(loaded_breakpoints[1].message, log_breakpoint.message);
1606 assert_eq!(loaded_breakpoints[1].condition, log_breakpoint.condition);
1607 assert_eq!(
1608 loaded_breakpoints[1].hit_condition,
1609 log_breakpoint.hit_condition
1610 );
1611 assert_eq!(loaded_breakpoints[1].state, log_breakpoint.state);
1612 assert_eq!(loaded_breakpoints[1].path, Arc::from(path));
1613
1614 // disable breakpoint
1615 assert_eq!(loaded_breakpoints[2].row, disable_breakpoint.position);
1616 assert_eq!(loaded_breakpoints[2].message, disable_breakpoint.message);
1617 assert_eq!(
1618 loaded_breakpoints[2].condition,
1619 disable_breakpoint.condition
1620 );
1621 assert_eq!(
1622 loaded_breakpoints[2].hit_condition,
1623 disable_breakpoint.hit_condition
1624 );
1625 assert_eq!(loaded_breakpoints[2].state, disable_breakpoint.state);
1626 assert_eq!(loaded_breakpoints[2].path, Arc::from(path));
1627
1628 // condition breakpoint
1629 assert_eq!(loaded_breakpoints[3].row, condition_breakpoint.position);
1630 assert_eq!(loaded_breakpoints[3].message, condition_breakpoint.message);
1631 assert_eq!(
1632 loaded_breakpoints[3].condition,
1633 condition_breakpoint.condition
1634 );
1635 assert_eq!(
1636 loaded_breakpoints[3].hit_condition,
1637 condition_breakpoint.hit_condition
1638 );
1639 assert_eq!(loaded_breakpoints[3].state, condition_breakpoint.state);
1640 assert_eq!(loaded_breakpoints[3].path, Arc::from(path));
1641
1642 // hit condition breakpoint
1643 assert_eq!(loaded_breakpoints[4].row, hit_condition_breakpoint.position);
1644 assert_eq!(
1645 loaded_breakpoints[4].message,
1646 hit_condition_breakpoint.message
1647 );
1648 assert_eq!(
1649 loaded_breakpoints[4].condition,
1650 hit_condition_breakpoint.condition
1651 );
1652 assert_eq!(
1653 loaded_breakpoints[4].hit_condition,
1654 hit_condition_breakpoint.hit_condition
1655 );
1656 assert_eq!(loaded_breakpoints[4].state, hit_condition_breakpoint.state);
1657 assert_eq!(loaded_breakpoints[4].path, Arc::from(path));
1658 }
1659
1660 #[gpui::test]
1661 async fn test_remove_last_breakpoint() {
1662 zlog::init_test();
1663
1664 let db = WorkspaceDb::open_test_db("test_remove_last_breakpoint").await;
1665 let id = db.next_id().await.unwrap();
1666
1667 let singular_path = Path::new("/tmp/test_remove_last_breakpoint.rs");
1668
1669 let breakpoint_to_remove = Breakpoint {
1670 position: 100,
1671 message: None,
1672 state: BreakpointState::Enabled,
1673 condition: None,
1674 hit_condition: None,
1675 };
1676
1677 let workspace = SerializedWorkspace {
1678 id,
1679 location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]),
1680 center_group: Default::default(),
1681 window_bounds: Default::default(),
1682 display: Default::default(),
1683 docks: Default::default(),
1684 centered_layout: false,
1685 breakpoints: {
1686 let mut map = collections::BTreeMap::default();
1687 map.insert(
1688 Arc::from(singular_path),
1689 vec![SourceBreakpoint {
1690 row: breakpoint_to_remove.position,
1691 path: Arc::from(singular_path),
1692 message: None,
1693 state: BreakpointState::Enabled,
1694 condition: None,
1695 hit_condition: None,
1696 }],
1697 );
1698 map
1699 },
1700 session_id: None,
1701 window_id: None,
1702 };
1703
1704 db.save_workspace(workspace.clone()).await;
1705
1706 let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
1707 let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(singular_path)).unwrap();
1708
1709 assert_eq!(loaded_breakpoints.len(), 1);
1710 assert_eq!(loaded_breakpoints[0].row, breakpoint_to_remove.position);
1711 assert_eq!(loaded_breakpoints[0].message, breakpoint_to_remove.message);
1712 assert_eq!(
1713 loaded_breakpoints[0].condition,
1714 breakpoint_to_remove.condition
1715 );
1716 assert_eq!(
1717 loaded_breakpoints[0].hit_condition,
1718 breakpoint_to_remove.hit_condition
1719 );
1720 assert_eq!(loaded_breakpoints[0].state, breakpoint_to_remove.state);
1721 assert_eq!(loaded_breakpoints[0].path, Arc::from(singular_path));
1722
1723 let workspace_without_breakpoint = SerializedWorkspace {
1724 id,
1725 location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]),
1726 center_group: Default::default(),
1727 window_bounds: Default::default(),
1728 display: Default::default(),
1729 docks: Default::default(),
1730 centered_layout: false,
1731 breakpoints: collections::BTreeMap::default(),
1732 session_id: None,
1733 window_id: None,
1734 };
1735
1736 db.save_workspace(workspace_without_breakpoint.clone())
1737 .await;
1738
1739 let loaded_after_remove = db.workspace_for_roots(&["/tmp"]).unwrap();
1740 let empty_breakpoints = loaded_after_remove
1741 .breakpoints
1742 .get(&Arc::from(singular_path));
1743
1744 assert!(empty_breakpoints.is_none());
1745 }
1746
1747 #[gpui::test]
1748 async fn test_next_id_stability() {
1749 zlog::init_test();
1750
1751 let db = WorkspaceDb::open_test_db("test_next_id_stability").await;
1752
1753 db.write(|conn| {
1754 conn.migrate(
1755 "test_table",
1756 &[sql!(
1757 CREATE TABLE test_table(
1758 text TEXT,
1759 workspace_id INTEGER,
1760 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
1761 ON DELETE CASCADE
1762 ) STRICT;
1763 )],
1764 )
1765 .unwrap();
1766 })
1767 .await;
1768
1769 let id = db.next_id().await.unwrap();
1770 // Assert the empty row got inserted
1771 assert_eq!(
1772 Some(id),
1773 db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
1774 SELECT workspace_id FROM workspaces WHERE workspace_id = ?
1775 ))
1776 .unwrap()(id)
1777 .unwrap()
1778 );
1779
1780 db.write(move |conn| {
1781 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
1782 .unwrap()(("test-text-1", id))
1783 .unwrap()
1784 })
1785 .await;
1786
1787 let test_text_1 = db
1788 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
1789 .unwrap()(1)
1790 .unwrap()
1791 .unwrap();
1792 assert_eq!(test_text_1, "test-text-1");
1793 }
1794
1795 #[gpui::test]
1796 async fn test_workspace_id_stability() {
1797 zlog::init_test();
1798
1799 let db = WorkspaceDb::open_test_db("test_workspace_id_stability").await;
1800
1801 db.write(|conn| {
1802 conn.migrate(
1803 "test_table",
1804 &[sql!(
1805 CREATE TABLE test_table(
1806 text TEXT,
1807 workspace_id INTEGER,
1808 FOREIGN KEY(workspace_id)
1809 REFERENCES workspaces(workspace_id)
1810 ON DELETE CASCADE
1811 ) STRICT;)],
1812 )
1813 })
1814 .await
1815 .unwrap();
1816
1817 let mut workspace_1 = SerializedWorkspace {
1818 id: WorkspaceId(1),
1819 location: SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp2"]),
1820 center_group: Default::default(),
1821 window_bounds: Default::default(),
1822 display: Default::default(),
1823 docks: Default::default(),
1824 centered_layout: false,
1825 breakpoints: Default::default(),
1826 session_id: None,
1827 window_id: None,
1828 };
1829
1830 let workspace_2 = SerializedWorkspace {
1831 id: WorkspaceId(2),
1832 location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]),
1833 center_group: Default::default(),
1834 window_bounds: Default::default(),
1835 display: Default::default(),
1836 docks: Default::default(),
1837 centered_layout: false,
1838 breakpoints: Default::default(),
1839 session_id: None,
1840 window_id: None,
1841 };
1842
1843 db.save_workspace(workspace_1.clone()).await;
1844
1845 db.write(|conn| {
1846 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
1847 .unwrap()(("test-text-1", 1))
1848 .unwrap();
1849 })
1850 .await;
1851
1852 db.save_workspace(workspace_2.clone()).await;
1853
1854 db.write(|conn| {
1855 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
1856 .unwrap()(("test-text-2", 2))
1857 .unwrap();
1858 })
1859 .await;
1860
1861 workspace_1.location = SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp3"]);
1862 db.save_workspace(workspace_1.clone()).await;
1863 db.save_workspace(workspace_1).await;
1864 db.save_workspace(workspace_2).await;
1865
1866 let test_text_2 = db
1867 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
1868 .unwrap()(2)
1869 .unwrap()
1870 .unwrap();
1871 assert_eq!(test_text_2, "test-text-2");
1872
1873 let test_text_1 = db
1874 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
1875 .unwrap()(1)
1876 .unwrap()
1877 .unwrap();
1878 assert_eq!(test_text_1, "test-text-1");
1879 }
1880
1881 fn group(axis: Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
1882 SerializedPaneGroup::Group {
1883 axis: SerializedAxis(axis),
1884 flexes: None,
1885 children,
1886 }
1887 }
1888
1889 #[gpui::test]
1890 async fn test_full_workspace_serialization() {
1891 zlog::init_test();
1892
1893 let db = WorkspaceDb::open_test_db("test_full_workspace_serialization").await;
1894
1895 // -----------------
1896 // | 1,2 | 5,6 |
1897 // | - - - | |
1898 // | 3,4 | |
1899 // -----------------
1900 let center_group = group(
1901 Axis::Horizontal,
1902 vec![
1903 group(
1904 Axis::Vertical,
1905 vec![
1906 SerializedPaneGroup::Pane(SerializedPane::new(
1907 vec![
1908 SerializedItem::new("Terminal", 5, false, false),
1909 SerializedItem::new("Terminal", 6, true, false),
1910 ],
1911 false,
1912 0,
1913 )),
1914 SerializedPaneGroup::Pane(SerializedPane::new(
1915 vec![
1916 SerializedItem::new("Terminal", 7, true, false),
1917 SerializedItem::new("Terminal", 8, false, false),
1918 ],
1919 false,
1920 0,
1921 )),
1922 ],
1923 ),
1924 SerializedPaneGroup::Pane(SerializedPane::new(
1925 vec![
1926 SerializedItem::new("Terminal", 9, false, false),
1927 SerializedItem::new("Terminal", 10, true, false),
1928 ],
1929 false,
1930 0,
1931 )),
1932 ],
1933 );
1934
1935 let workspace = SerializedWorkspace {
1936 id: WorkspaceId(5),
1937 location: SerializedWorkspaceLocation::Local(
1938 LocalPaths::new(["/tmp", "/tmp2"]),
1939 LocalPathsOrder::new([1, 0]),
1940 ),
1941 center_group,
1942 window_bounds: Default::default(),
1943 breakpoints: Default::default(),
1944 display: Default::default(),
1945 docks: Default::default(),
1946 centered_layout: false,
1947 session_id: None,
1948 window_id: Some(999),
1949 };
1950
1951 db.save_workspace(workspace.clone()).await;
1952
1953 let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
1954 assert_eq!(workspace, round_trip_workspace.unwrap());
1955
1956 // Test guaranteed duplicate IDs
1957 db.save_workspace(workspace.clone()).await;
1958 db.save_workspace(workspace.clone()).await;
1959
1960 let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
1961 assert_eq!(workspace, round_trip_workspace.unwrap());
1962 }
1963
1964 #[gpui::test]
1965 async fn test_workspace_assignment() {
1966 zlog::init_test();
1967
1968 let db = WorkspaceDb::open_test_db("test_basic_functionality").await;
1969
1970 let workspace_1 = SerializedWorkspace {
1971 id: WorkspaceId(1),
1972 location: SerializedWorkspaceLocation::Local(
1973 LocalPaths::new(["/tmp", "/tmp2"]),
1974 LocalPathsOrder::new([0, 1]),
1975 ),
1976 center_group: Default::default(),
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(1),
1984 };
1985
1986 let mut workspace_2 = SerializedWorkspace {
1987 id: WorkspaceId(2),
1988 location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]),
1989 center_group: Default::default(),
1990 window_bounds: Default::default(),
1991 display: Default::default(),
1992 docks: Default::default(),
1993 centered_layout: false,
1994 breakpoints: Default::default(),
1995 session_id: None,
1996 window_id: Some(2),
1997 };
1998
1999 db.save_workspace(workspace_1.clone()).await;
2000 db.save_workspace(workspace_2.clone()).await;
2001
2002 // Test that paths are treated as a set
2003 assert_eq!(
2004 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2005 workspace_1
2006 );
2007 assert_eq!(
2008 db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
2009 workspace_1
2010 );
2011
2012 // Make sure that other keys work
2013 assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
2014 assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
2015
2016 // Test 'mutate' case of updating a pre-existing id
2017 workspace_2.location = SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp2"]);
2018
2019 db.save_workspace(workspace_2.clone()).await;
2020 assert_eq!(
2021 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2022 workspace_2
2023 );
2024
2025 // Test other mechanism for mutating
2026 let mut workspace_3 = SerializedWorkspace {
2027 id: WorkspaceId(3),
2028 location: SerializedWorkspaceLocation::Local(
2029 LocalPaths::new(["/tmp", "/tmp2"]),
2030 LocalPathsOrder::new([1, 0]),
2031 ),
2032 center_group: Default::default(),
2033 window_bounds: Default::default(),
2034 breakpoints: Default::default(),
2035 display: Default::default(),
2036 docks: Default::default(),
2037 centered_layout: false,
2038 session_id: None,
2039 window_id: Some(3),
2040 };
2041
2042 db.save_workspace(workspace_3.clone()).await;
2043 assert_eq!(
2044 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2045 workspace_3
2046 );
2047
2048 // Make sure that updating paths differently also works
2049 workspace_3.location =
2050 SerializedWorkspaceLocation::from_local_paths(["/tmp3", "/tmp4", "/tmp2"]);
2051 db.save_workspace(workspace_3.clone()).await;
2052 assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
2053 assert_eq!(
2054 db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
2055 .unwrap(),
2056 workspace_3
2057 );
2058 }
2059
2060 #[gpui::test]
2061 async fn test_session_workspaces() {
2062 zlog::init_test();
2063
2064 let db = WorkspaceDb::open_test_db("test_serializing_workspaces_session_id").await;
2065
2066 let workspace_1 = SerializedWorkspace {
2067 id: WorkspaceId(1),
2068 location: SerializedWorkspaceLocation::from_local_paths(["/tmp1"]),
2069 center_group: Default::default(),
2070 window_bounds: Default::default(),
2071 display: Default::default(),
2072 docks: Default::default(),
2073 centered_layout: false,
2074 breakpoints: Default::default(),
2075 session_id: Some("session-id-1".to_owned()),
2076 window_id: Some(10),
2077 };
2078
2079 let workspace_2 = SerializedWorkspace {
2080 id: WorkspaceId(2),
2081 location: SerializedWorkspaceLocation::from_local_paths(["/tmp2"]),
2082 center_group: Default::default(),
2083 window_bounds: Default::default(),
2084 display: Default::default(),
2085 docks: Default::default(),
2086 centered_layout: false,
2087 breakpoints: Default::default(),
2088 session_id: Some("session-id-1".to_owned()),
2089 window_id: Some(20),
2090 };
2091
2092 let workspace_3 = SerializedWorkspace {
2093 id: WorkspaceId(3),
2094 location: SerializedWorkspaceLocation::from_local_paths(["/tmp3"]),
2095 center_group: Default::default(),
2096 window_bounds: Default::default(),
2097 display: Default::default(),
2098 docks: Default::default(),
2099 centered_layout: false,
2100 breakpoints: Default::default(),
2101 session_id: Some("session-id-2".to_owned()),
2102 window_id: Some(30),
2103 };
2104
2105 let workspace_4 = SerializedWorkspace {
2106 id: WorkspaceId(4),
2107 location: SerializedWorkspaceLocation::from_local_paths(["/tmp4"]),
2108 center_group: Default::default(),
2109 window_bounds: Default::default(),
2110 display: Default::default(),
2111 docks: Default::default(),
2112 centered_layout: false,
2113 breakpoints: Default::default(),
2114 session_id: None,
2115 window_id: None,
2116 };
2117
2118 let ssh_project = db
2119 .get_or_create_ssh_project("my-host".to_string(), Some(1234), vec![], None)
2120 .await
2121 .unwrap();
2122
2123 let workspace_5 = SerializedWorkspace {
2124 id: WorkspaceId(5),
2125 location: SerializedWorkspaceLocation::Ssh(ssh_project.clone()),
2126 center_group: Default::default(),
2127 window_bounds: Default::default(),
2128 display: Default::default(),
2129 docks: Default::default(),
2130 centered_layout: false,
2131 breakpoints: Default::default(),
2132 session_id: Some("session-id-2".to_owned()),
2133 window_id: Some(50),
2134 };
2135
2136 let workspace_6 = SerializedWorkspace {
2137 id: WorkspaceId(6),
2138 location: SerializedWorkspaceLocation::Local(
2139 LocalPaths::new(["/tmp6a", "/tmp6b", "/tmp6c"]),
2140 LocalPathsOrder::new([2, 1, 0]),
2141 ),
2142 center_group: Default::default(),
2143 window_bounds: Default::default(),
2144 breakpoints: Default::default(),
2145 display: Default::default(),
2146 docks: Default::default(),
2147 centered_layout: false,
2148 session_id: Some("session-id-3".to_owned()),
2149 window_id: Some(60),
2150 };
2151
2152 db.save_workspace(workspace_1.clone()).await;
2153 thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
2154 db.save_workspace(workspace_2.clone()).await;
2155 db.save_workspace(workspace_3.clone()).await;
2156 thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
2157 db.save_workspace(workspace_4.clone()).await;
2158 db.save_workspace(workspace_5.clone()).await;
2159 db.save_workspace(workspace_6.clone()).await;
2160
2161 let locations = db.session_workspaces("session-id-1".to_owned()).unwrap();
2162 assert_eq!(locations.len(), 2);
2163 assert_eq!(locations[0].0, LocalPaths::new(["/tmp2"]));
2164 assert_eq!(locations[0].1, LocalPathsOrder::new([0]));
2165 assert_eq!(locations[0].2, Some(20));
2166 assert_eq!(locations[1].0, LocalPaths::new(["/tmp1"]));
2167 assert_eq!(locations[1].1, LocalPathsOrder::new([0]));
2168 assert_eq!(locations[1].2, Some(10));
2169
2170 let locations = db.session_workspaces("session-id-2".to_owned()).unwrap();
2171 assert_eq!(locations.len(), 2);
2172 let empty_paths: Vec<&str> = Vec::new();
2173 assert_eq!(locations[0].0, LocalPaths::new(empty_paths.iter()));
2174 assert_eq!(locations[0].1, LocalPathsOrder::new([]));
2175 assert_eq!(locations[0].2, Some(50));
2176 assert_eq!(locations[0].3, Some(ssh_project.id.0));
2177 assert_eq!(locations[1].0, LocalPaths::new(["/tmp3"]));
2178 assert_eq!(locations[1].1, LocalPathsOrder::new([0]));
2179 assert_eq!(locations[1].2, Some(30));
2180
2181 let locations = db.session_workspaces("session-id-3".to_owned()).unwrap();
2182 assert_eq!(locations.len(), 1);
2183 assert_eq!(
2184 locations[0].0,
2185 LocalPaths::new(["/tmp6a", "/tmp6b", "/tmp6c"]),
2186 );
2187 assert_eq!(locations[0].1, LocalPathsOrder::new([2, 1, 0]));
2188 assert_eq!(locations[0].2, Some(60));
2189 }
2190
2191 fn default_workspace<P: AsRef<Path>>(
2192 workspace_id: &[P],
2193 center_group: &SerializedPaneGroup,
2194 ) -> SerializedWorkspace {
2195 SerializedWorkspace {
2196 id: WorkspaceId(4),
2197 location: SerializedWorkspaceLocation::from_local_paths(workspace_id),
2198 center_group: center_group.clone(),
2199 window_bounds: Default::default(),
2200 display: Default::default(),
2201 docks: Default::default(),
2202 breakpoints: Default::default(),
2203 centered_layout: false,
2204 session_id: None,
2205 window_id: None,
2206 }
2207 }
2208
2209 #[gpui::test]
2210 async fn test_last_session_workspace_locations() {
2211 let dir1 = tempfile::TempDir::with_prefix("dir1").unwrap();
2212 let dir2 = tempfile::TempDir::with_prefix("dir2").unwrap();
2213 let dir3 = tempfile::TempDir::with_prefix("dir3").unwrap();
2214 let dir4 = tempfile::TempDir::with_prefix("dir4").unwrap();
2215
2216 let db =
2217 WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces").await;
2218
2219 let workspaces = [
2220 (1, vec![dir1.path()], vec![0], 9),
2221 (2, vec![dir2.path()], vec![0], 5),
2222 (3, vec![dir3.path()], vec![0], 8),
2223 (4, vec![dir4.path()], vec![0], 2),
2224 (
2225 5,
2226 vec![dir1.path(), dir2.path(), dir3.path()],
2227 vec![0, 1, 2],
2228 3,
2229 ),
2230 (
2231 6,
2232 vec![dir2.path(), dir3.path(), dir4.path()],
2233 vec![2, 1, 0],
2234 4,
2235 ),
2236 ]
2237 .into_iter()
2238 .map(|(id, locations, order, window_id)| SerializedWorkspace {
2239 id: WorkspaceId(id),
2240 location: SerializedWorkspaceLocation::Local(
2241 LocalPaths::new(locations),
2242 LocalPathsOrder::new(order),
2243 ),
2244 center_group: Default::default(),
2245 window_bounds: Default::default(),
2246 display: Default::default(),
2247 docks: Default::default(),
2248 centered_layout: false,
2249 session_id: Some("one-session".to_owned()),
2250 breakpoints: Default::default(),
2251 window_id: Some(window_id),
2252 })
2253 .collect::<Vec<_>>();
2254
2255 for workspace in workspaces.iter() {
2256 db.save_workspace(workspace.clone()).await;
2257 }
2258
2259 let stack = Some(Vec::from([
2260 WindowId::from(2), // Top
2261 WindowId::from(8),
2262 WindowId::from(5),
2263 WindowId::from(9),
2264 WindowId::from(3),
2265 WindowId::from(4), // Bottom
2266 ]));
2267
2268 let have = db
2269 .last_session_workspace_locations("one-session", stack)
2270 .unwrap();
2271 assert_eq!(have.len(), 6);
2272 assert_eq!(
2273 have[0],
2274 SerializedWorkspaceLocation::from_local_paths(&[dir4.path()])
2275 );
2276 assert_eq!(
2277 have[1],
2278 SerializedWorkspaceLocation::from_local_paths([dir3.path()])
2279 );
2280 assert_eq!(
2281 have[2],
2282 SerializedWorkspaceLocation::from_local_paths([dir2.path()])
2283 );
2284 assert_eq!(
2285 have[3],
2286 SerializedWorkspaceLocation::from_local_paths([dir1.path()])
2287 );
2288 assert_eq!(
2289 have[4],
2290 SerializedWorkspaceLocation::Local(
2291 LocalPaths::new([dir1.path(), dir2.path(), dir3.path()]),
2292 LocalPathsOrder::new([0, 1, 2]),
2293 ),
2294 );
2295 assert_eq!(
2296 have[5],
2297 SerializedWorkspaceLocation::Local(
2298 LocalPaths::new([dir2.path(), dir3.path(), dir4.path()]),
2299 LocalPathsOrder::new([2, 1, 0]),
2300 ),
2301 );
2302 }
2303
2304 #[gpui::test]
2305 async fn test_last_session_workspace_locations_ssh_projects() {
2306 let db = WorkspaceDb::open_test_db(
2307 "test_serializing_workspaces_last_session_workspaces_ssh_projects",
2308 )
2309 .await;
2310
2311 let ssh_projects = [
2312 ("host-1", "my-user-1"),
2313 ("host-2", "my-user-2"),
2314 ("host-3", "my-user-3"),
2315 ("host-4", "my-user-4"),
2316 ]
2317 .into_iter()
2318 .map(|(host, user)| async {
2319 db.get_or_create_ssh_project(host.to_string(), None, vec![], Some(user.to_string()))
2320 .await
2321 .unwrap()
2322 })
2323 .collect::<Vec<_>>();
2324
2325 let ssh_projects = futures::future::join_all(ssh_projects).await;
2326
2327 let workspaces = [
2328 (1, ssh_projects[0].clone(), 9),
2329 (2, ssh_projects[1].clone(), 5),
2330 (3, ssh_projects[2].clone(), 8),
2331 (4, ssh_projects[3].clone(), 2),
2332 ]
2333 .into_iter()
2334 .map(|(id, ssh_project, window_id)| SerializedWorkspace {
2335 id: WorkspaceId(id),
2336 location: SerializedWorkspaceLocation::Ssh(ssh_project),
2337 center_group: Default::default(),
2338 window_bounds: Default::default(),
2339 display: Default::default(),
2340 docks: Default::default(),
2341 centered_layout: false,
2342 session_id: Some("one-session".to_owned()),
2343 breakpoints: Default::default(),
2344 window_id: Some(window_id),
2345 })
2346 .collect::<Vec<_>>();
2347
2348 for workspace in workspaces.iter() {
2349 db.save_workspace(workspace.clone()).await;
2350 }
2351
2352 let stack = Some(Vec::from([
2353 WindowId::from(2), // Top
2354 WindowId::from(8),
2355 WindowId::from(5),
2356 WindowId::from(9), // Bottom
2357 ]));
2358
2359 let have = db
2360 .last_session_workspace_locations("one-session", stack)
2361 .unwrap();
2362 assert_eq!(have.len(), 4);
2363 assert_eq!(
2364 have[0],
2365 SerializedWorkspaceLocation::Ssh(ssh_projects[3].clone())
2366 );
2367 assert_eq!(
2368 have[1],
2369 SerializedWorkspaceLocation::Ssh(ssh_projects[2].clone())
2370 );
2371 assert_eq!(
2372 have[2],
2373 SerializedWorkspaceLocation::Ssh(ssh_projects[1].clone())
2374 );
2375 assert_eq!(
2376 have[3],
2377 SerializedWorkspaceLocation::Ssh(ssh_projects[0].clone())
2378 );
2379 }
2380
2381 #[gpui::test]
2382 async fn test_get_or_create_ssh_project() {
2383 let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project").await;
2384
2385 let (host, port, paths, user) = (
2386 "example.com".to_string(),
2387 Some(22_u16),
2388 vec!["/home/user".to_string(), "/etc/nginx".to_string()],
2389 Some("user".to_string()),
2390 );
2391
2392 let project = db
2393 .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone())
2394 .await
2395 .unwrap();
2396
2397 assert_eq!(project.host, host);
2398 assert_eq!(project.paths, paths);
2399 assert_eq!(project.user, user);
2400
2401 // Test that calling the function again with the same parameters returns the same project
2402 let same_project = db
2403 .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone())
2404 .await
2405 .unwrap();
2406
2407 assert_eq!(project.id, same_project.id);
2408
2409 // Test with different parameters
2410 let (host2, paths2, user2) = (
2411 "otherexample.com".to_string(),
2412 vec!["/home/otheruser".to_string()],
2413 Some("otheruser".to_string()),
2414 );
2415
2416 let different_project = db
2417 .get_or_create_ssh_project(host2.clone(), None, paths2.clone(), user2.clone())
2418 .await
2419 .unwrap();
2420
2421 assert_ne!(project.id, different_project.id);
2422 assert_eq!(different_project.host, host2);
2423 assert_eq!(different_project.paths, paths2);
2424 assert_eq!(different_project.user, user2);
2425 }
2426
2427 #[gpui::test]
2428 async fn test_get_or_create_ssh_project_with_null_user() {
2429 let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project_with_null_user").await;
2430
2431 let (host, port, paths, user) = (
2432 "example.com".to_string(),
2433 None,
2434 vec!["/home/user".to_string()],
2435 None,
2436 );
2437
2438 let project = db
2439 .get_or_create_ssh_project(host.clone(), port, paths.clone(), None)
2440 .await
2441 .unwrap();
2442
2443 assert_eq!(project.host, host);
2444 assert_eq!(project.paths, paths);
2445 assert_eq!(project.user, None);
2446
2447 // Test that calling the function again with the same parameters returns the same project
2448 let same_project = db
2449 .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone())
2450 .await
2451 .unwrap();
2452
2453 assert_eq!(project.id, same_project.id);
2454 }
2455
2456 #[gpui::test]
2457 async fn test_get_ssh_projects() {
2458 let db = WorkspaceDb::open_test_db("test_get_ssh_projects").await;
2459
2460 let projects = vec![
2461 (
2462 "example.com".to_string(),
2463 None,
2464 vec!["/home/user".to_string()],
2465 None,
2466 ),
2467 (
2468 "anotherexample.com".to_string(),
2469 Some(123_u16),
2470 vec!["/home/user2".to_string()],
2471 Some("user2".to_string()),
2472 ),
2473 (
2474 "yetanother.com".to_string(),
2475 Some(345_u16),
2476 vec!["/home/user3".to_string(), "/proc/1234/exe".to_string()],
2477 None,
2478 ),
2479 ];
2480
2481 for (host, port, paths, user) in projects.iter() {
2482 let project = db
2483 .get_or_create_ssh_project(host.clone(), *port, paths.clone(), user.clone())
2484 .await
2485 .unwrap();
2486
2487 assert_eq!(&project.host, host);
2488 assert_eq!(&project.port, port);
2489 assert_eq!(&project.paths, paths);
2490 assert_eq!(&project.user, user);
2491 }
2492
2493 let stored_projects = db.ssh_projects().unwrap();
2494 assert_eq!(stored_projects.len(), projects.len());
2495 }
2496
2497 #[gpui::test]
2498 async fn test_simple_split() {
2499 zlog::init_test();
2500
2501 let db = WorkspaceDb::open_test_db("simple_split").await;
2502
2503 // -----------------
2504 // | 1,2 | 5,6 |
2505 // | - - - | |
2506 // | 3,4 | |
2507 // -----------------
2508 let center_pane = group(
2509 Axis::Horizontal,
2510 vec![
2511 group(
2512 Axis::Vertical,
2513 vec![
2514 SerializedPaneGroup::Pane(SerializedPane::new(
2515 vec![
2516 SerializedItem::new("Terminal", 1, false, false),
2517 SerializedItem::new("Terminal", 2, true, false),
2518 ],
2519 false,
2520 0,
2521 )),
2522 SerializedPaneGroup::Pane(SerializedPane::new(
2523 vec![
2524 SerializedItem::new("Terminal", 4, false, false),
2525 SerializedItem::new("Terminal", 3, true, false),
2526 ],
2527 true,
2528 0,
2529 )),
2530 ],
2531 ),
2532 SerializedPaneGroup::Pane(SerializedPane::new(
2533 vec![
2534 SerializedItem::new("Terminal", 5, true, false),
2535 SerializedItem::new("Terminal", 6, false, false),
2536 ],
2537 false,
2538 0,
2539 )),
2540 ],
2541 );
2542
2543 let workspace = default_workspace(&["/tmp"], ¢er_pane);
2544
2545 db.save_workspace(workspace.clone()).await;
2546
2547 let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
2548
2549 assert_eq!(workspace.center_group, new_workspace.center_group);
2550 }
2551
2552 #[gpui::test]
2553 async fn test_cleanup_panes() {
2554 zlog::init_test();
2555
2556 let db = WorkspaceDb::open_test_db("test_cleanup_panes").await;
2557
2558 let center_pane = group(
2559 Axis::Horizontal,
2560 vec![
2561 group(
2562 Axis::Vertical,
2563 vec![
2564 SerializedPaneGroup::Pane(SerializedPane::new(
2565 vec![
2566 SerializedItem::new("Terminal", 1, false, false),
2567 SerializedItem::new("Terminal", 2, true, false),
2568 ],
2569 false,
2570 0,
2571 )),
2572 SerializedPaneGroup::Pane(SerializedPane::new(
2573 vec![
2574 SerializedItem::new("Terminal", 4, false, false),
2575 SerializedItem::new("Terminal", 3, true, false),
2576 ],
2577 true,
2578 0,
2579 )),
2580 ],
2581 ),
2582 SerializedPaneGroup::Pane(SerializedPane::new(
2583 vec![
2584 SerializedItem::new("Terminal", 5, false, false),
2585 SerializedItem::new("Terminal", 6, true, false),
2586 ],
2587 false,
2588 0,
2589 )),
2590 ],
2591 );
2592
2593 let id = &["/tmp"];
2594
2595 let mut workspace = default_workspace(id, ¢er_pane);
2596
2597 db.save_workspace(workspace.clone()).await;
2598
2599 workspace.center_group = group(
2600 Axis::Vertical,
2601 vec![
2602 SerializedPaneGroup::Pane(SerializedPane::new(
2603 vec![
2604 SerializedItem::new("Terminal", 1, false, false),
2605 SerializedItem::new("Terminal", 2, true, false),
2606 ],
2607 false,
2608 0,
2609 )),
2610 SerializedPaneGroup::Pane(SerializedPane::new(
2611 vec![
2612 SerializedItem::new("Terminal", 4, true, false),
2613 SerializedItem::new("Terminal", 3, false, false),
2614 ],
2615 true,
2616 0,
2617 )),
2618 ],
2619 );
2620
2621 db.save_workspace(workspace.clone()).await;
2622
2623 let new_workspace = db.workspace_for_roots(id).unwrap();
2624
2625 assert_eq!(workspace.center_group, new_workspace.center_group);
2626 }
2627}