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 language_name: LanguageName,
1338 ) -> Result<Option<Toolchain>> {
1339 self.write(move |this| {
1340 let mut select = this
1341 .select_bound(sql!(
1342 SELECT name, path, raw_json FROM toolchains WHERE workspace_id = ? AND language_name = ? AND worktree_id = ?
1343 ))
1344 .context("Preparing insertion")?;
1345
1346 let toolchain: Vec<(String, String, String)> =
1347 select((workspace_id, language_name.as_ref().to_string(), worktree_id.to_usize()))?;
1348
1349 Ok(toolchain.into_iter().next().and_then(|(name, path, raw_json)| Some(Toolchain {
1350 name: name.into(),
1351 path: path.into(),
1352 language_name,
1353 as_json: serde_json::Value::from_str(&raw_json).ok()?
1354 })))
1355 })
1356 .await
1357 }
1358
1359 pub(crate) async fn toolchains(
1360 &self,
1361 workspace_id: WorkspaceId,
1362 ) -> Result<Vec<(Toolchain, WorktreeId, Arc<Path>)>> {
1363 self.write(move |this| {
1364 let mut select = this
1365 .select_bound(sql!(
1366 SELECT name, path, worktree_id, relative_worktree_path, language_name, raw_json FROM toolchains WHERE workspace_id = ?
1367 ))
1368 .context("Preparing insertion")?;
1369
1370 let toolchain: Vec<(String, String, u64, String, String, String)> =
1371 select(workspace_id)?;
1372
1373 Ok(toolchain.into_iter().filter_map(|(name, path, worktree_id, relative_worktree_path, language_name, raw_json)| Some((Toolchain {
1374 name: name.into(),
1375 path: path.into(),
1376 language_name: LanguageName::new(&language_name),
1377 as_json: serde_json::Value::from_str(&raw_json).ok()?
1378 }, WorktreeId::from_proto(worktree_id), Arc::from(relative_worktree_path.as_ref())))).collect())
1379 })
1380 .await
1381 }
1382 pub async fn set_toolchain(
1383 &self,
1384 workspace_id: WorkspaceId,
1385 worktree_id: WorktreeId,
1386 relative_worktree_path: String,
1387 toolchain: Toolchain,
1388 ) -> Result<()> {
1389 self.write(move |conn| {
1390 let mut insert = conn
1391 .exec_bound(sql!(
1392 INSERT INTO toolchains(workspace_id, worktree_id, relative_worktree_path, language_name, name, path) VALUES (?, ?, ?, ?, ?, ?)
1393 ON CONFLICT DO
1394 UPDATE SET
1395 name = ?4,
1396 path = ?5
1397
1398 ))
1399 .context("Preparing insertion")?;
1400
1401 insert((
1402 workspace_id,
1403 worktree_id.to_usize(),
1404 relative_worktree_path,
1405 toolchain.language_name.as_ref(),
1406 toolchain.name.as_ref(),
1407 toolchain.path.as_ref(),
1408 ))?;
1409
1410 Ok(())
1411 }).await
1412 }
1413}
1414
1415#[cfg(test)]
1416mod tests {
1417 use std::thread;
1418 use std::time::Duration;
1419
1420 use super::*;
1421 use crate::persistence::model::SerializedWorkspace;
1422 use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup};
1423 use db::open_test_db;
1424 use gpui;
1425
1426 #[gpui::test]
1427 async fn test_breakpoints() {
1428 env_logger::try_init().ok();
1429
1430 let db = WorkspaceDb(open_test_db("test_breakpoints").await);
1431 let id = db.next_id().await.unwrap();
1432
1433 let path = Path::new("/tmp/test.rs");
1434
1435 let breakpoint = Breakpoint {
1436 position: 123,
1437 message: None,
1438 state: BreakpointState::Enabled,
1439 condition: None,
1440 hit_condition: None,
1441 };
1442
1443 let log_breakpoint = Breakpoint {
1444 position: 456,
1445 message: Some("Test log message".into()),
1446 state: BreakpointState::Enabled,
1447 condition: None,
1448 hit_condition: None,
1449 };
1450
1451 let disable_breakpoint = Breakpoint {
1452 position: 578,
1453 message: None,
1454 state: BreakpointState::Disabled,
1455 condition: None,
1456 hit_condition: None,
1457 };
1458
1459 let condition_breakpoint = Breakpoint {
1460 position: 789,
1461 message: None,
1462 state: BreakpointState::Enabled,
1463 condition: Some("x > 5".into()),
1464 hit_condition: None,
1465 };
1466
1467 let hit_condition_breakpoint = Breakpoint {
1468 position: 999,
1469 message: None,
1470 state: BreakpointState::Enabled,
1471 condition: None,
1472 hit_condition: Some(">= 3".into()),
1473 };
1474
1475 let workspace = SerializedWorkspace {
1476 id,
1477 location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]),
1478 center_group: Default::default(),
1479 window_bounds: Default::default(),
1480 display: Default::default(),
1481 docks: Default::default(),
1482 centered_layout: false,
1483 breakpoints: {
1484 let mut map = collections::BTreeMap::default();
1485 map.insert(
1486 Arc::from(path),
1487 vec![
1488 SourceBreakpoint {
1489 row: breakpoint.position,
1490 path: Arc::from(path),
1491 message: breakpoint.message.clone(),
1492 state: breakpoint.state,
1493 condition: breakpoint.condition.clone(),
1494 hit_condition: breakpoint.hit_condition.clone(),
1495 },
1496 SourceBreakpoint {
1497 row: log_breakpoint.position,
1498 path: Arc::from(path),
1499 message: log_breakpoint.message.clone(),
1500 state: log_breakpoint.state,
1501 condition: log_breakpoint.condition.clone(),
1502 hit_condition: log_breakpoint.hit_condition.clone(),
1503 },
1504 SourceBreakpoint {
1505 row: disable_breakpoint.position,
1506 path: Arc::from(path),
1507 message: disable_breakpoint.message.clone(),
1508 state: disable_breakpoint.state,
1509 condition: disable_breakpoint.condition.clone(),
1510 hit_condition: disable_breakpoint.hit_condition.clone(),
1511 },
1512 SourceBreakpoint {
1513 row: condition_breakpoint.position,
1514 path: Arc::from(path),
1515 message: condition_breakpoint.message.clone(),
1516 state: condition_breakpoint.state,
1517 condition: condition_breakpoint.condition.clone(),
1518 hit_condition: condition_breakpoint.hit_condition.clone(),
1519 },
1520 SourceBreakpoint {
1521 row: hit_condition_breakpoint.position,
1522 path: Arc::from(path),
1523 message: hit_condition_breakpoint.message.clone(),
1524 state: hit_condition_breakpoint.state,
1525 condition: hit_condition_breakpoint.condition.clone(),
1526 hit_condition: hit_condition_breakpoint.hit_condition.clone(),
1527 },
1528 ],
1529 );
1530 map
1531 },
1532 session_id: None,
1533 window_id: None,
1534 };
1535
1536 db.save_workspace(workspace.clone()).await;
1537
1538 let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
1539 let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(path)).unwrap();
1540
1541 assert_eq!(loaded_breakpoints.len(), 5);
1542
1543 // normal breakpoint
1544 assert_eq!(loaded_breakpoints[0].row, breakpoint.position);
1545 assert_eq!(loaded_breakpoints[0].message, breakpoint.message);
1546 assert_eq!(loaded_breakpoints[0].condition, breakpoint.condition);
1547 assert_eq!(
1548 loaded_breakpoints[0].hit_condition,
1549 breakpoint.hit_condition
1550 );
1551 assert_eq!(loaded_breakpoints[0].state, breakpoint.state);
1552 assert_eq!(loaded_breakpoints[0].path, Arc::from(path));
1553
1554 // enabled breakpoint
1555 assert_eq!(loaded_breakpoints[1].row, log_breakpoint.position);
1556 assert_eq!(loaded_breakpoints[1].message, log_breakpoint.message);
1557 assert_eq!(loaded_breakpoints[1].condition, log_breakpoint.condition);
1558 assert_eq!(
1559 loaded_breakpoints[1].hit_condition,
1560 log_breakpoint.hit_condition
1561 );
1562 assert_eq!(loaded_breakpoints[1].state, log_breakpoint.state);
1563 assert_eq!(loaded_breakpoints[1].path, Arc::from(path));
1564
1565 // disable breakpoint
1566 assert_eq!(loaded_breakpoints[2].row, disable_breakpoint.position);
1567 assert_eq!(loaded_breakpoints[2].message, disable_breakpoint.message);
1568 assert_eq!(
1569 loaded_breakpoints[2].condition,
1570 disable_breakpoint.condition
1571 );
1572 assert_eq!(
1573 loaded_breakpoints[2].hit_condition,
1574 disable_breakpoint.hit_condition
1575 );
1576 assert_eq!(loaded_breakpoints[2].state, disable_breakpoint.state);
1577 assert_eq!(loaded_breakpoints[2].path, Arc::from(path));
1578
1579 // condition breakpoint
1580 assert_eq!(loaded_breakpoints[3].row, condition_breakpoint.position);
1581 assert_eq!(loaded_breakpoints[3].message, condition_breakpoint.message);
1582 assert_eq!(
1583 loaded_breakpoints[3].condition,
1584 condition_breakpoint.condition
1585 );
1586 assert_eq!(
1587 loaded_breakpoints[3].hit_condition,
1588 condition_breakpoint.hit_condition
1589 );
1590 assert_eq!(loaded_breakpoints[3].state, condition_breakpoint.state);
1591 assert_eq!(loaded_breakpoints[3].path, Arc::from(path));
1592
1593 // hit condition breakpoint
1594 assert_eq!(loaded_breakpoints[4].row, hit_condition_breakpoint.position);
1595 assert_eq!(
1596 loaded_breakpoints[4].message,
1597 hit_condition_breakpoint.message
1598 );
1599 assert_eq!(
1600 loaded_breakpoints[4].condition,
1601 hit_condition_breakpoint.condition
1602 );
1603 assert_eq!(
1604 loaded_breakpoints[4].hit_condition,
1605 hit_condition_breakpoint.hit_condition
1606 );
1607 assert_eq!(loaded_breakpoints[4].state, hit_condition_breakpoint.state);
1608 assert_eq!(loaded_breakpoints[4].path, Arc::from(path));
1609 }
1610
1611 #[gpui::test]
1612 async fn test_remove_last_breakpoint() {
1613 env_logger::try_init().ok();
1614
1615 let db = WorkspaceDb(open_test_db("test_remove_last_breakpoint").await);
1616 let id = db.next_id().await.unwrap();
1617
1618 let singular_path = Path::new("/tmp/test_remove_last_breakpoint.rs");
1619
1620 let breakpoint_to_remove = Breakpoint {
1621 position: 100,
1622 message: None,
1623 state: BreakpointState::Enabled,
1624 condition: None,
1625 hit_condition: None,
1626 };
1627
1628 let workspace = SerializedWorkspace {
1629 id,
1630 location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]),
1631 center_group: Default::default(),
1632 window_bounds: Default::default(),
1633 display: Default::default(),
1634 docks: Default::default(),
1635 centered_layout: false,
1636 breakpoints: {
1637 let mut map = collections::BTreeMap::default();
1638 map.insert(
1639 Arc::from(singular_path),
1640 vec![SourceBreakpoint {
1641 row: breakpoint_to_remove.position,
1642 path: Arc::from(singular_path),
1643 message: None,
1644 state: BreakpointState::Enabled,
1645 condition: None,
1646 hit_condition: None,
1647 }],
1648 );
1649 map
1650 },
1651 session_id: None,
1652 window_id: None,
1653 };
1654
1655 db.save_workspace(workspace.clone()).await;
1656
1657 let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
1658 let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(singular_path)).unwrap();
1659
1660 assert_eq!(loaded_breakpoints.len(), 1);
1661 assert_eq!(loaded_breakpoints[0].row, breakpoint_to_remove.position);
1662 assert_eq!(loaded_breakpoints[0].message, breakpoint_to_remove.message);
1663 assert_eq!(
1664 loaded_breakpoints[0].condition,
1665 breakpoint_to_remove.condition
1666 );
1667 assert_eq!(
1668 loaded_breakpoints[0].hit_condition,
1669 breakpoint_to_remove.hit_condition
1670 );
1671 assert_eq!(loaded_breakpoints[0].state, breakpoint_to_remove.state);
1672 assert_eq!(loaded_breakpoints[0].path, Arc::from(singular_path));
1673
1674 let workspace_without_breakpoint = SerializedWorkspace {
1675 id,
1676 location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]),
1677 center_group: Default::default(),
1678 window_bounds: Default::default(),
1679 display: Default::default(),
1680 docks: Default::default(),
1681 centered_layout: false,
1682 breakpoints: collections::BTreeMap::default(),
1683 session_id: None,
1684 window_id: None,
1685 };
1686
1687 db.save_workspace(workspace_without_breakpoint.clone())
1688 .await;
1689
1690 let loaded_after_remove = db.workspace_for_roots(&["/tmp"]).unwrap();
1691 let empty_breakpoints = loaded_after_remove
1692 .breakpoints
1693 .get(&Arc::from(singular_path));
1694
1695 assert!(empty_breakpoints.is_none());
1696 }
1697
1698 #[gpui::test]
1699 async fn test_next_id_stability() {
1700 env_logger::try_init().ok();
1701
1702 let db = WorkspaceDb(open_test_db("test_next_id_stability").await);
1703
1704 db.write(|conn| {
1705 conn.migrate(
1706 "test_table",
1707 &[sql!(
1708 CREATE TABLE test_table(
1709 text TEXT,
1710 workspace_id INTEGER,
1711 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
1712 ON DELETE CASCADE
1713 ) STRICT;
1714 )],
1715 )
1716 .unwrap();
1717 })
1718 .await;
1719
1720 let id = db.next_id().await.unwrap();
1721 // Assert the empty row got inserted
1722 assert_eq!(
1723 Some(id),
1724 db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
1725 SELECT workspace_id FROM workspaces WHERE workspace_id = ?
1726 ))
1727 .unwrap()(id)
1728 .unwrap()
1729 );
1730
1731 db.write(move |conn| {
1732 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
1733 .unwrap()(("test-text-1", id))
1734 .unwrap()
1735 })
1736 .await;
1737
1738 let test_text_1 = db
1739 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
1740 .unwrap()(1)
1741 .unwrap()
1742 .unwrap();
1743 assert_eq!(test_text_1, "test-text-1");
1744 }
1745
1746 #[gpui::test]
1747 async fn test_workspace_id_stability() {
1748 env_logger::try_init().ok();
1749
1750 let db = WorkspaceDb(open_test_db("test_workspace_id_stability").await);
1751
1752 db.write(|conn| {
1753 conn.migrate(
1754 "test_table",
1755 &[sql!(
1756 CREATE TABLE test_table(
1757 text TEXT,
1758 workspace_id INTEGER,
1759 FOREIGN KEY(workspace_id)
1760 REFERENCES workspaces(workspace_id)
1761 ON DELETE CASCADE
1762 ) STRICT;)],
1763 )
1764 })
1765 .await
1766 .unwrap();
1767
1768 let mut workspace_1 = SerializedWorkspace {
1769 id: WorkspaceId(1),
1770 location: SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp2"]),
1771 center_group: Default::default(),
1772 window_bounds: Default::default(),
1773 display: Default::default(),
1774 docks: Default::default(),
1775 centered_layout: false,
1776 breakpoints: Default::default(),
1777 session_id: None,
1778 window_id: None,
1779 };
1780
1781 let workspace_2 = SerializedWorkspace {
1782 id: WorkspaceId(2),
1783 location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]),
1784 center_group: Default::default(),
1785 window_bounds: Default::default(),
1786 display: Default::default(),
1787 docks: Default::default(),
1788 centered_layout: false,
1789 breakpoints: Default::default(),
1790 session_id: None,
1791 window_id: None,
1792 };
1793
1794 db.save_workspace(workspace_1.clone()).await;
1795
1796 db.write(|conn| {
1797 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
1798 .unwrap()(("test-text-1", 1))
1799 .unwrap();
1800 })
1801 .await;
1802
1803 db.save_workspace(workspace_2.clone()).await;
1804
1805 db.write(|conn| {
1806 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
1807 .unwrap()(("test-text-2", 2))
1808 .unwrap();
1809 })
1810 .await;
1811
1812 workspace_1.location = SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp3"]);
1813 db.save_workspace(workspace_1.clone()).await;
1814 db.save_workspace(workspace_1).await;
1815 db.save_workspace(workspace_2).await;
1816
1817 let test_text_2 = db
1818 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
1819 .unwrap()(2)
1820 .unwrap()
1821 .unwrap();
1822 assert_eq!(test_text_2, "test-text-2");
1823
1824 let test_text_1 = db
1825 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
1826 .unwrap()(1)
1827 .unwrap()
1828 .unwrap();
1829 assert_eq!(test_text_1, "test-text-1");
1830 }
1831
1832 fn group(axis: Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
1833 SerializedPaneGroup::Group {
1834 axis: SerializedAxis(axis),
1835 flexes: None,
1836 children,
1837 }
1838 }
1839
1840 #[gpui::test]
1841 async fn test_full_workspace_serialization() {
1842 env_logger::try_init().ok();
1843
1844 let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await);
1845
1846 // -----------------
1847 // | 1,2 | 5,6 |
1848 // | - - - | |
1849 // | 3,4 | |
1850 // -----------------
1851 let center_group = group(
1852 Axis::Horizontal,
1853 vec![
1854 group(
1855 Axis::Vertical,
1856 vec![
1857 SerializedPaneGroup::Pane(SerializedPane::new(
1858 vec![
1859 SerializedItem::new("Terminal", 5, false, false),
1860 SerializedItem::new("Terminal", 6, true, false),
1861 ],
1862 false,
1863 0,
1864 )),
1865 SerializedPaneGroup::Pane(SerializedPane::new(
1866 vec![
1867 SerializedItem::new("Terminal", 7, true, false),
1868 SerializedItem::new("Terminal", 8, false, false),
1869 ],
1870 false,
1871 0,
1872 )),
1873 ],
1874 ),
1875 SerializedPaneGroup::Pane(SerializedPane::new(
1876 vec![
1877 SerializedItem::new("Terminal", 9, false, false),
1878 SerializedItem::new("Terminal", 10, true, false),
1879 ],
1880 false,
1881 0,
1882 )),
1883 ],
1884 );
1885
1886 let workspace = SerializedWorkspace {
1887 id: WorkspaceId(5),
1888 location: SerializedWorkspaceLocation::Local(
1889 LocalPaths::new(["/tmp", "/tmp2"]),
1890 LocalPathsOrder::new([1, 0]),
1891 ),
1892 center_group,
1893 window_bounds: Default::default(),
1894 breakpoints: Default::default(),
1895 display: Default::default(),
1896 docks: Default::default(),
1897 centered_layout: false,
1898 session_id: None,
1899 window_id: Some(999),
1900 };
1901
1902 db.save_workspace(workspace.clone()).await;
1903
1904 let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
1905 assert_eq!(workspace, round_trip_workspace.unwrap());
1906
1907 // Test guaranteed duplicate IDs
1908 db.save_workspace(workspace.clone()).await;
1909 db.save_workspace(workspace.clone()).await;
1910
1911 let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
1912 assert_eq!(workspace, round_trip_workspace.unwrap());
1913 }
1914
1915 #[gpui::test]
1916 async fn test_workspace_assignment() {
1917 env_logger::try_init().ok();
1918
1919 let db = WorkspaceDb(open_test_db("test_basic_functionality").await);
1920
1921 let workspace_1 = SerializedWorkspace {
1922 id: WorkspaceId(1),
1923 location: SerializedWorkspaceLocation::Local(
1924 LocalPaths::new(["/tmp", "/tmp2"]),
1925 LocalPathsOrder::new([0, 1]),
1926 ),
1927 center_group: Default::default(),
1928 window_bounds: Default::default(),
1929 breakpoints: Default::default(),
1930 display: Default::default(),
1931 docks: Default::default(),
1932 centered_layout: false,
1933 session_id: None,
1934 window_id: Some(1),
1935 };
1936
1937 let mut workspace_2 = SerializedWorkspace {
1938 id: WorkspaceId(2),
1939 location: SerializedWorkspaceLocation::from_local_paths(["/tmp"]),
1940 center_group: Default::default(),
1941 window_bounds: Default::default(),
1942 display: Default::default(),
1943 docks: Default::default(),
1944 centered_layout: false,
1945 breakpoints: Default::default(),
1946 session_id: None,
1947 window_id: Some(2),
1948 };
1949
1950 db.save_workspace(workspace_1.clone()).await;
1951 db.save_workspace(workspace_2.clone()).await;
1952
1953 // Test that paths are treated as a set
1954 assert_eq!(
1955 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
1956 workspace_1
1957 );
1958 assert_eq!(
1959 db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
1960 workspace_1
1961 );
1962
1963 // Make sure that other keys work
1964 assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
1965 assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
1966
1967 // Test 'mutate' case of updating a pre-existing id
1968 workspace_2.location = SerializedWorkspaceLocation::from_local_paths(["/tmp", "/tmp2"]);
1969
1970 db.save_workspace(workspace_2.clone()).await;
1971 assert_eq!(
1972 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
1973 workspace_2
1974 );
1975
1976 // Test other mechanism for mutating
1977 let mut workspace_3 = SerializedWorkspace {
1978 id: WorkspaceId(3),
1979 location: SerializedWorkspaceLocation::Local(
1980 LocalPaths::new(["/tmp", "/tmp2"]),
1981 LocalPathsOrder::new([1, 0]),
1982 ),
1983 center_group: Default::default(),
1984 window_bounds: Default::default(),
1985 breakpoints: Default::default(),
1986 display: Default::default(),
1987 docks: Default::default(),
1988 centered_layout: false,
1989 session_id: None,
1990 window_id: Some(3),
1991 };
1992
1993 db.save_workspace(workspace_3.clone()).await;
1994 assert_eq!(
1995 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
1996 workspace_3
1997 );
1998
1999 // Make sure that updating paths differently also works
2000 workspace_3.location =
2001 SerializedWorkspaceLocation::from_local_paths(["/tmp3", "/tmp4", "/tmp2"]);
2002 db.save_workspace(workspace_3.clone()).await;
2003 assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
2004 assert_eq!(
2005 db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
2006 .unwrap(),
2007 workspace_3
2008 );
2009 }
2010
2011 #[gpui::test]
2012 async fn test_session_workspaces() {
2013 env_logger::try_init().ok();
2014
2015 let db = WorkspaceDb(open_test_db("test_serializing_workspaces_session_id").await);
2016
2017 let workspace_1 = SerializedWorkspace {
2018 id: WorkspaceId(1),
2019 location: SerializedWorkspaceLocation::from_local_paths(["/tmp1"]),
2020 center_group: Default::default(),
2021 window_bounds: Default::default(),
2022 display: Default::default(),
2023 docks: Default::default(),
2024 centered_layout: false,
2025 breakpoints: Default::default(),
2026 session_id: Some("session-id-1".to_owned()),
2027 window_id: Some(10),
2028 };
2029
2030 let workspace_2 = SerializedWorkspace {
2031 id: WorkspaceId(2),
2032 location: SerializedWorkspaceLocation::from_local_paths(["/tmp2"]),
2033 center_group: Default::default(),
2034 window_bounds: Default::default(),
2035 display: Default::default(),
2036 docks: Default::default(),
2037 centered_layout: false,
2038 breakpoints: Default::default(),
2039 session_id: Some("session-id-1".to_owned()),
2040 window_id: Some(20),
2041 };
2042
2043 let workspace_3 = SerializedWorkspace {
2044 id: WorkspaceId(3),
2045 location: SerializedWorkspaceLocation::from_local_paths(["/tmp3"]),
2046 center_group: Default::default(),
2047 window_bounds: Default::default(),
2048 display: Default::default(),
2049 docks: Default::default(),
2050 centered_layout: false,
2051 breakpoints: Default::default(),
2052 session_id: Some("session-id-2".to_owned()),
2053 window_id: Some(30),
2054 };
2055
2056 let workspace_4 = SerializedWorkspace {
2057 id: WorkspaceId(4),
2058 location: SerializedWorkspaceLocation::from_local_paths(["/tmp4"]),
2059 center_group: Default::default(),
2060 window_bounds: Default::default(),
2061 display: Default::default(),
2062 docks: Default::default(),
2063 centered_layout: false,
2064 breakpoints: Default::default(),
2065 session_id: None,
2066 window_id: None,
2067 };
2068
2069 let ssh_project = db
2070 .get_or_create_ssh_project("my-host".to_string(), Some(1234), vec![], None)
2071 .await
2072 .unwrap();
2073
2074 let workspace_5 = SerializedWorkspace {
2075 id: WorkspaceId(5),
2076 location: SerializedWorkspaceLocation::Ssh(ssh_project.clone()),
2077 center_group: Default::default(),
2078 window_bounds: Default::default(),
2079 display: Default::default(),
2080 docks: Default::default(),
2081 centered_layout: false,
2082 breakpoints: Default::default(),
2083 session_id: Some("session-id-2".to_owned()),
2084 window_id: Some(50),
2085 };
2086
2087 let workspace_6 = SerializedWorkspace {
2088 id: WorkspaceId(6),
2089 location: SerializedWorkspaceLocation::Local(
2090 LocalPaths::new(["/tmp6a", "/tmp6b", "/tmp6c"]),
2091 LocalPathsOrder::new([2, 1, 0]),
2092 ),
2093 center_group: Default::default(),
2094 window_bounds: Default::default(),
2095 breakpoints: Default::default(),
2096 display: Default::default(),
2097 docks: Default::default(),
2098 centered_layout: false,
2099 session_id: Some("session-id-3".to_owned()),
2100 window_id: Some(60),
2101 };
2102
2103 db.save_workspace(workspace_1.clone()).await;
2104 thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
2105 db.save_workspace(workspace_2.clone()).await;
2106 db.save_workspace(workspace_3.clone()).await;
2107 thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
2108 db.save_workspace(workspace_4.clone()).await;
2109 db.save_workspace(workspace_5.clone()).await;
2110 db.save_workspace(workspace_6.clone()).await;
2111
2112 let locations = db.session_workspaces("session-id-1".to_owned()).unwrap();
2113 assert_eq!(locations.len(), 2);
2114 assert_eq!(locations[0].0, LocalPaths::new(["/tmp2"]));
2115 assert_eq!(locations[0].1, LocalPathsOrder::new([0]));
2116 assert_eq!(locations[0].2, Some(20));
2117 assert_eq!(locations[1].0, LocalPaths::new(["/tmp1"]));
2118 assert_eq!(locations[1].1, LocalPathsOrder::new([0]));
2119 assert_eq!(locations[1].2, Some(10));
2120
2121 let locations = db.session_workspaces("session-id-2".to_owned()).unwrap();
2122 assert_eq!(locations.len(), 2);
2123 let empty_paths: Vec<&str> = Vec::new();
2124 assert_eq!(locations[0].0, LocalPaths::new(empty_paths.iter()));
2125 assert_eq!(locations[0].1, LocalPathsOrder::new([]));
2126 assert_eq!(locations[0].2, Some(50));
2127 assert_eq!(locations[0].3, Some(ssh_project.id.0));
2128 assert_eq!(locations[1].0, LocalPaths::new(["/tmp3"]));
2129 assert_eq!(locations[1].1, LocalPathsOrder::new([0]));
2130 assert_eq!(locations[1].2, Some(30));
2131
2132 let locations = db.session_workspaces("session-id-3".to_owned()).unwrap();
2133 assert_eq!(locations.len(), 1);
2134 assert_eq!(
2135 locations[0].0,
2136 LocalPaths::new(["/tmp6a", "/tmp6b", "/tmp6c"]),
2137 );
2138 assert_eq!(locations[0].1, LocalPathsOrder::new([2, 1, 0]));
2139 assert_eq!(locations[0].2, Some(60));
2140 }
2141
2142 fn default_workspace<P: AsRef<Path>>(
2143 workspace_id: &[P],
2144 center_group: &SerializedPaneGroup,
2145 ) -> SerializedWorkspace {
2146 SerializedWorkspace {
2147 id: WorkspaceId(4),
2148 location: SerializedWorkspaceLocation::from_local_paths(workspace_id),
2149 center_group: center_group.clone(),
2150 window_bounds: Default::default(),
2151 display: Default::default(),
2152 docks: Default::default(),
2153 breakpoints: Default::default(),
2154 centered_layout: false,
2155 session_id: None,
2156 window_id: None,
2157 }
2158 }
2159
2160 #[gpui::test]
2161 async fn test_last_session_workspace_locations() {
2162 let dir1 = tempfile::TempDir::with_prefix("dir1").unwrap();
2163 let dir2 = tempfile::TempDir::with_prefix("dir2").unwrap();
2164 let dir3 = tempfile::TempDir::with_prefix("dir3").unwrap();
2165 let dir4 = tempfile::TempDir::with_prefix("dir4").unwrap();
2166
2167 let db =
2168 WorkspaceDb(open_test_db("test_serializing_workspaces_last_session_workspaces").await);
2169
2170 let workspaces = [
2171 (1, vec![dir1.path()], vec![0], 9),
2172 (2, vec![dir2.path()], vec![0], 5),
2173 (3, vec![dir3.path()], vec![0], 8),
2174 (4, vec![dir4.path()], vec![0], 2),
2175 (
2176 5,
2177 vec![dir1.path(), dir2.path(), dir3.path()],
2178 vec![0, 1, 2],
2179 3,
2180 ),
2181 (
2182 6,
2183 vec![dir2.path(), dir3.path(), dir4.path()],
2184 vec![2, 1, 0],
2185 4,
2186 ),
2187 ]
2188 .into_iter()
2189 .map(|(id, locations, order, window_id)| SerializedWorkspace {
2190 id: WorkspaceId(id),
2191 location: SerializedWorkspaceLocation::Local(
2192 LocalPaths::new(locations),
2193 LocalPathsOrder::new(order),
2194 ),
2195 center_group: Default::default(),
2196 window_bounds: Default::default(),
2197 display: Default::default(),
2198 docks: Default::default(),
2199 centered_layout: false,
2200 session_id: Some("one-session".to_owned()),
2201 breakpoints: Default::default(),
2202 window_id: Some(window_id),
2203 })
2204 .collect::<Vec<_>>();
2205
2206 for workspace in workspaces.iter() {
2207 db.save_workspace(workspace.clone()).await;
2208 }
2209
2210 let stack = Some(Vec::from([
2211 WindowId::from(2), // Top
2212 WindowId::from(8),
2213 WindowId::from(5),
2214 WindowId::from(9),
2215 WindowId::from(3),
2216 WindowId::from(4), // Bottom
2217 ]));
2218
2219 let have = db
2220 .last_session_workspace_locations("one-session", stack)
2221 .unwrap();
2222 assert_eq!(have.len(), 6);
2223 assert_eq!(
2224 have[0],
2225 SerializedWorkspaceLocation::from_local_paths(&[dir4.path()])
2226 );
2227 assert_eq!(
2228 have[1],
2229 SerializedWorkspaceLocation::from_local_paths([dir3.path()])
2230 );
2231 assert_eq!(
2232 have[2],
2233 SerializedWorkspaceLocation::from_local_paths([dir2.path()])
2234 );
2235 assert_eq!(
2236 have[3],
2237 SerializedWorkspaceLocation::from_local_paths([dir1.path()])
2238 );
2239 assert_eq!(
2240 have[4],
2241 SerializedWorkspaceLocation::Local(
2242 LocalPaths::new([dir1.path(), dir2.path(), dir3.path()]),
2243 LocalPathsOrder::new([0, 1, 2]),
2244 ),
2245 );
2246 assert_eq!(
2247 have[5],
2248 SerializedWorkspaceLocation::Local(
2249 LocalPaths::new([dir2.path(), dir3.path(), dir4.path()]),
2250 LocalPathsOrder::new([2, 1, 0]),
2251 ),
2252 );
2253 }
2254
2255 #[gpui::test]
2256 async fn test_last_session_workspace_locations_ssh_projects() {
2257 let db = WorkspaceDb(
2258 open_test_db("test_serializing_workspaces_last_session_workspaces_ssh_projects").await,
2259 );
2260
2261 let ssh_projects = [
2262 ("host-1", "my-user-1"),
2263 ("host-2", "my-user-2"),
2264 ("host-3", "my-user-3"),
2265 ("host-4", "my-user-4"),
2266 ]
2267 .into_iter()
2268 .map(|(host, user)| async {
2269 db.get_or_create_ssh_project(host.to_string(), None, vec![], Some(user.to_string()))
2270 .await
2271 .unwrap()
2272 })
2273 .collect::<Vec<_>>();
2274
2275 let ssh_projects = futures::future::join_all(ssh_projects).await;
2276
2277 let workspaces = [
2278 (1, ssh_projects[0].clone(), 9),
2279 (2, ssh_projects[1].clone(), 5),
2280 (3, ssh_projects[2].clone(), 8),
2281 (4, ssh_projects[3].clone(), 2),
2282 ]
2283 .into_iter()
2284 .map(|(id, ssh_project, window_id)| SerializedWorkspace {
2285 id: WorkspaceId(id),
2286 location: SerializedWorkspaceLocation::Ssh(ssh_project),
2287 center_group: Default::default(),
2288 window_bounds: Default::default(),
2289 display: Default::default(),
2290 docks: Default::default(),
2291 centered_layout: false,
2292 session_id: Some("one-session".to_owned()),
2293 breakpoints: Default::default(),
2294 window_id: Some(window_id),
2295 })
2296 .collect::<Vec<_>>();
2297
2298 for workspace in workspaces.iter() {
2299 db.save_workspace(workspace.clone()).await;
2300 }
2301
2302 let stack = Some(Vec::from([
2303 WindowId::from(2), // Top
2304 WindowId::from(8),
2305 WindowId::from(5),
2306 WindowId::from(9), // Bottom
2307 ]));
2308
2309 let have = db
2310 .last_session_workspace_locations("one-session", stack)
2311 .unwrap();
2312 assert_eq!(have.len(), 4);
2313 assert_eq!(
2314 have[0],
2315 SerializedWorkspaceLocation::Ssh(ssh_projects[3].clone())
2316 );
2317 assert_eq!(
2318 have[1],
2319 SerializedWorkspaceLocation::Ssh(ssh_projects[2].clone())
2320 );
2321 assert_eq!(
2322 have[2],
2323 SerializedWorkspaceLocation::Ssh(ssh_projects[1].clone())
2324 );
2325 assert_eq!(
2326 have[3],
2327 SerializedWorkspaceLocation::Ssh(ssh_projects[0].clone())
2328 );
2329 }
2330
2331 #[gpui::test]
2332 async fn test_get_or_create_ssh_project() {
2333 let db = WorkspaceDb(open_test_db("test_get_or_create_ssh_project").await);
2334
2335 let (host, port, paths, user) = (
2336 "example.com".to_string(),
2337 Some(22_u16),
2338 vec!["/home/user".to_string(), "/etc/nginx".to_string()],
2339 Some("user".to_string()),
2340 );
2341
2342 let project = db
2343 .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone())
2344 .await
2345 .unwrap();
2346
2347 assert_eq!(project.host, host);
2348 assert_eq!(project.paths, paths);
2349 assert_eq!(project.user, user);
2350
2351 // Test that calling the function again with the same parameters returns the same project
2352 let same_project = db
2353 .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone())
2354 .await
2355 .unwrap();
2356
2357 assert_eq!(project.id, same_project.id);
2358
2359 // Test with different parameters
2360 let (host2, paths2, user2) = (
2361 "otherexample.com".to_string(),
2362 vec!["/home/otheruser".to_string()],
2363 Some("otheruser".to_string()),
2364 );
2365
2366 let different_project = db
2367 .get_or_create_ssh_project(host2.clone(), None, paths2.clone(), user2.clone())
2368 .await
2369 .unwrap();
2370
2371 assert_ne!(project.id, different_project.id);
2372 assert_eq!(different_project.host, host2);
2373 assert_eq!(different_project.paths, paths2);
2374 assert_eq!(different_project.user, user2);
2375 }
2376
2377 #[gpui::test]
2378 async fn test_get_or_create_ssh_project_with_null_user() {
2379 let db = WorkspaceDb(open_test_db("test_get_or_create_ssh_project_with_null_user").await);
2380
2381 let (host, port, paths, user) = (
2382 "example.com".to_string(),
2383 None,
2384 vec!["/home/user".to_string()],
2385 None,
2386 );
2387
2388 let project = db
2389 .get_or_create_ssh_project(host.clone(), port, paths.clone(), None)
2390 .await
2391 .unwrap();
2392
2393 assert_eq!(project.host, host);
2394 assert_eq!(project.paths, paths);
2395 assert_eq!(project.user, None);
2396
2397 // Test that calling the function again with the same parameters returns the same project
2398 let same_project = db
2399 .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone())
2400 .await
2401 .unwrap();
2402
2403 assert_eq!(project.id, same_project.id);
2404 }
2405
2406 #[gpui::test]
2407 async fn test_get_ssh_projects() {
2408 let db = WorkspaceDb(open_test_db("test_get_ssh_projects").await);
2409
2410 let projects = vec![
2411 (
2412 "example.com".to_string(),
2413 None,
2414 vec!["/home/user".to_string()],
2415 None,
2416 ),
2417 (
2418 "anotherexample.com".to_string(),
2419 Some(123_u16),
2420 vec!["/home/user2".to_string()],
2421 Some("user2".to_string()),
2422 ),
2423 (
2424 "yetanother.com".to_string(),
2425 Some(345_u16),
2426 vec!["/home/user3".to_string(), "/proc/1234/exe".to_string()],
2427 None,
2428 ),
2429 ];
2430
2431 for (host, port, paths, user) in projects.iter() {
2432 let project = db
2433 .get_or_create_ssh_project(host.clone(), *port, paths.clone(), user.clone())
2434 .await
2435 .unwrap();
2436
2437 assert_eq!(&project.host, host);
2438 assert_eq!(&project.port, port);
2439 assert_eq!(&project.paths, paths);
2440 assert_eq!(&project.user, user);
2441 }
2442
2443 let stored_projects = db.ssh_projects().unwrap();
2444 assert_eq!(stored_projects.len(), projects.len());
2445 }
2446
2447 #[gpui::test]
2448 async fn test_simple_split() {
2449 env_logger::try_init().ok();
2450
2451 let db = WorkspaceDb(open_test_db("simple_split").await);
2452
2453 // -----------------
2454 // | 1,2 | 5,6 |
2455 // | - - - | |
2456 // | 3,4 | |
2457 // -----------------
2458 let center_pane = group(
2459 Axis::Horizontal,
2460 vec![
2461 group(
2462 Axis::Vertical,
2463 vec![
2464 SerializedPaneGroup::Pane(SerializedPane::new(
2465 vec![
2466 SerializedItem::new("Terminal", 1, false, false),
2467 SerializedItem::new("Terminal", 2, true, false),
2468 ],
2469 false,
2470 0,
2471 )),
2472 SerializedPaneGroup::Pane(SerializedPane::new(
2473 vec![
2474 SerializedItem::new("Terminal", 4, false, false),
2475 SerializedItem::new("Terminal", 3, true, false),
2476 ],
2477 true,
2478 0,
2479 )),
2480 ],
2481 ),
2482 SerializedPaneGroup::Pane(SerializedPane::new(
2483 vec![
2484 SerializedItem::new("Terminal", 5, true, false),
2485 SerializedItem::new("Terminal", 6, false, false),
2486 ],
2487 false,
2488 0,
2489 )),
2490 ],
2491 );
2492
2493 let workspace = default_workspace(&["/tmp"], ¢er_pane);
2494
2495 db.save_workspace(workspace.clone()).await;
2496
2497 let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
2498
2499 assert_eq!(workspace.center_group, new_workspace.center_group);
2500 }
2501
2502 #[gpui::test]
2503 async fn test_cleanup_panes() {
2504 env_logger::try_init().ok();
2505
2506 let db = WorkspaceDb(open_test_db("test_cleanup_panes").await);
2507
2508 let center_pane = group(
2509 Axis::Horizontal,
2510 vec![
2511 group(
2512 Axis::Vertical,
2513 vec![
2514 SerializedPaneGroup::Pane(SerializedPane::new(
2515 vec![
2516 SerializedItem::new("Terminal", 1, false, false),
2517 SerializedItem::new("Terminal", 2, true, false),
2518 ],
2519 false,
2520 0,
2521 )),
2522 SerializedPaneGroup::Pane(SerializedPane::new(
2523 vec![
2524 SerializedItem::new("Terminal", 4, false, false),
2525 SerializedItem::new("Terminal", 3, true, false),
2526 ],
2527 true,
2528 0,
2529 )),
2530 ],
2531 ),
2532 SerializedPaneGroup::Pane(SerializedPane::new(
2533 vec![
2534 SerializedItem::new("Terminal", 5, false, false),
2535 SerializedItem::new("Terminal", 6, true, false),
2536 ],
2537 false,
2538 0,
2539 )),
2540 ],
2541 );
2542
2543 let id = &["/tmp"];
2544
2545 let mut workspace = default_workspace(id, ¢er_pane);
2546
2547 db.save_workspace(workspace.clone()).await;
2548
2549 workspace.center_group = group(
2550 Axis::Vertical,
2551 vec![
2552 SerializedPaneGroup::Pane(SerializedPane::new(
2553 vec![
2554 SerializedItem::new("Terminal", 1, false, false),
2555 SerializedItem::new("Terminal", 2, true, false),
2556 ],
2557 false,
2558 0,
2559 )),
2560 SerializedPaneGroup::Pane(SerializedPane::new(
2561 vec![
2562 SerializedItem::new("Terminal", 4, true, false),
2563 SerializedItem::new("Terminal", 3, false, false),
2564 ],
2565 true,
2566 0,
2567 )),
2568 ],
2569 );
2570
2571 db.save_workspace(workspace.clone()).await;
2572
2573 let new_workspace = db.workspace_for_roots(id).unwrap();
2574
2575 assert_eq!(workspace.center_group, new_workspace.center_group);
2576 }
2577}