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