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