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