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