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