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