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