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, HashSet, IndexSet};
13use db::{
14 kvp::KEY_VALUE_STORE,
15 query,
16 sqlez::{connection::Connection, domain::Domain},
17 sqlez_macros::sql,
18};
19use gpui::{Axis, Bounds, Task, WindowBounds, WindowId, point, size};
20use project::{
21 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
22 trusted_worktrees::{DbTrustedPaths, RemoteHostLocation},
23};
24
25use language::{LanguageName, Toolchain, ToolchainScope};
26use remote::{
27 DockerConnectionOptions, RemoteConnectionOptions, SshConnectionOptions, WslConnectionOptions,
28};
29use serde::{Deserialize, Serialize};
30use sqlez::{
31 bindable::{Bind, Column, StaticColumnCount},
32 statement::Statement,
33 thread_safe_connection::ThreadSafeConnection,
34};
35
36use ui::{App, SharedString, px};
37use util::{ResultExt, maybe, rel_path::RelPath};
38use uuid::Uuid;
39
40use crate::{
41 WorkspaceId,
42 path_list::{PathList, SerializedPathList},
43 persistence::model::RemoteConnectionKind,
44};
45
46use model::{
47 GroupId, ItemId, PaneId, RemoteConnectionId, SerializedItem, SerializedPane,
48 SerializedPaneGroup, SerializedWorkspace,
49};
50
51use self::model::{DockStructure, SerializedWorkspaceLocation};
52
53// https://www.sqlite.org/limits.html
54// > <..> the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER,
55// > which defaults to <..> 32766 for SQLite versions after 3.32.0.
56const MAX_QUERY_PLACEHOLDERS: usize = 32000;
57
58#[derive(Copy, Clone, Debug, PartialEq)]
59pub(crate) struct SerializedAxis(pub(crate) gpui::Axis);
60impl sqlez::bindable::StaticColumnCount for SerializedAxis {}
61impl sqlez::bindable::Bind for SerializedAxis {
62 fn bind(
63 &self,
64 statement: &sqlez::statement::Statement,
65 start_index: i32,
66 ) -> anyhow::Result<i32> {
67 match self.0 {
68 gpui::Axis::Horizontal => "Horizontal",
69 gpui::Axis::Vertical => "Vertical",
70 }
71 .bind(statement, start_index)
72 }
73}
74
75impl sqlez::bindable::Column for SerializedAxis {
76 fn column(
77 statement: &mut sqlez::statement::Statement,
78 start_index: i32,
79 ) -> anyhow::Result<(Self, i32)> {
80 String::column(statement, start_index).and_then(|(axis_text, next_index)| {
81 Ok((
82 match axis_text.as_str() {
83 "Horizontal" => Self(Axis::Horizontal),
84 "Vertical" => Self(Axis::Vertical),
85 _ => anyhow::bail!("Stored serialized item kind is incorrect"),
86 },
87 next_index,
88 ))
89 })
90 }
91}
92
93#[derive(Copy, Clone, Debug, PartialEq, Default)]
94pub(crate) struct SerializedWindowBounds(pub(crate) WindowBounds);
95
96impl StaticColumnCount for SerializedWindowBounds {
97 fn column_count() -> usize {
98 5
99 }
100}
101
102impl Bind for SerializedWindowBounds {
103 fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
104 match self.0 {
105 WindowBounds::Windowed(bounds) => {
106 let next_index = statement.bind(&"Windowed", start_index)?;
107 statement.bind(
108 &(
109 SerializedPixels(bounds.origin.x),
110 SerializedPixels(bounds.origin.y),
111 SerializedPixels(bounds.size.width),
112 SerializedPixels(bounds.size.height),
113 ),
114 next_index,
115 )
116 }
117 WindowBounds::Maximized(bounds) => {
118 let next_index = statement.bind(&"Maximized", start_index)?;
119 statement.bind(
120 &(
121 SerializedPixels(bounds.origin.x),
122 SerializedPixels(bounds.origin.y),
123 SerializedPixels(bounds.size.width),
124 SerializedPixels(bounds.size.height),
125 ),
126 next_index,
127 )
128 }
129 WindowBounds::Fullscreen(bounds) => {
130 let next_index = statement.bind(&"FullScreen", start_index)?;
131 statement.bind(
132 &(
133 SerializedPixels(bounds.origin.x),
134 SerializedPixels(bounds.origin.y),
135 SerializedPixels(bounds.size.width),
136 SerializedPixels(bounds.size.height),
137 ),
138 next_index,
139 )
140 }
141 }
142 }
143}
144
145impl Column for SerializedWindowBounds {
146 fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
147 let (window_state, next_index) = String::column(statement, start_index)?;
148 let ((x, y, width, height), _): ((i32, i32, i32, i32), _) =
149 Column::column(statement, next_index)?;
150 let bounds = Bounds {
151 origin: point(px(x as f32), px(y as f32)),
152 size: size(px(width as f32), px(height as f32)),
153 };
154
155 let status = match window_state.as_str() {
156 "Windowed" | "Fixed" => SerializedWindowBounds(WindowBounds::Windowed(bounds)),
157 "Maximized" => SerializedWindowBounds(WindowBounds::Maximized(bounds)),
158 "FullScreen" => SerializedWindowBounds(WindowBounds::Fullscreen(bounds)),
159 _ => bail!("Window State did not have a valid string"),
160 };
161
162 Ok((status, next_index + 4))
163 }
164}
165
166const DEFAULT_WINDOW_BOUNDS_KEY: &str = "default_window_bounds";
167
168pub fn read_default_window_bounds() -> Option<(Uuid, WindowBounds)> {
169 let json_str = KEY_VALUE_STORE
170 .read_kvp(DEFAULT_WINDOW_BOUNDS_KEY)
171 .log_err()
172 .flatten()?;
173
174 let (display_uuid, persisted) =
175 serde_json::from_str::<(Uuid, WindowBoundsJson)>(&json_str).ok()?;
176 Some((display_uuid, persisted.into()))
177}
178
179pub async fn write_default_window_bounds(
180 bounds: WindowBounds,
181 display_uuid: Uuid,
182) -> anyhow::Result<()> {
183 let persisted = WindowBoundsJson::from(bounds);
184 let json_str = serde_json::to_string(&(display_uuid, persisted))?;
185 KEY_VALUE_STORE
186 .write_kvp(DEFAULT_WINDOW_BOUNDS_KEY.to_string(), json_str)
187 .await?;
188 Ok(())
189}
190
191#[derive(Serialize, Deserialize)]
192pub enum WindowBoundsJson {
193 Windowed {
194 x: i32,
195 y: i32,
196 width: i32,
197 height: i32,
198 },
199 Maximized {
200 x: i32,
201 y: i32,
202 width: i32,
203 height: i32,
204 },
205 Fullscreen {
206 x: i32,
207 y: i32,
208 width: i32,
209 height: i32,
210 },
211}
212
213impl From<WindowBounds> for WindowBoundsJson {
214 fn from(b: WindowBounds) -> Self {
215 match b {
216 WindowBounds::Windowed(bounds) => {
217 let origin = bounds.origin;
218 let size = bounds.size;
219 WindowBoundsJson::Windowed {
220 x: f32::from(origin.x).round() as i32,
221 y: f32::from(origin.y).round() as i32,
222 width: f32::from(size.width).round() as i32,
223 height: f32::from(size.height).round() as i32,
224 }
225 }
226 WindowBounds::Maximized(bounds) => {
227 let origin = bounds.origin;
228 let size = bounds.size;
229 WindowBoundsJson::Maximized {
230 x: f32::from(origin.x).round() as i32,
231 y: f32::from(origin.y).round() as i32,
232 width: f32::from(size.width).round() as i32,
233 height: f32::from(size.height).round() as i32,
234 }
235 }
236 WindowBounds::Fullscreen(bounds) => {
237 let origin = bounds.origin;
238 let size = bounds.size;
239 WindowBoundsJson::Fullscreen {
240 x: f32::from(origin.x).round() as i32,
241 y: f32::from(origin.y).round() as i32,
242 width: f32::from(size.width).round() as i32,
243 height: f32::from(size.height).round() as i32,
244 }
245 }
246 }
247 }
248}
249
250impl From<WindowBoundsJson> for WindowBounds {
251 fn from(n: WindowBoundsJson) -> Self {
252 match n {
253 WindowBoundsJson::Windowed {
254 x,
255 y,
256 width,
257 height,
258 } => WindowBounds::Windowed(Bounds {
259 origin: point(px(x as f32), px(y as f32)),
260 size: size(px(width as f32), px(height as f32)),
261 }),
262 WindowBoundsJson::Maximized {
263 x,
264 y,
265 width,
266 height,
267 } => WindowBounds::Maximized(Bounds {
268 origin: point(px(x as f32), px(y as f32)),
269 size: size(px(width as f32), px(height as f32)),
270 }),
271 WindowBoundsJson::Fullscreen {
272 x,
273 y,
274 width,
275 height,
276 } => WindowBounds::Fullscreen(Bounds {
277 origin: point(px(x as f32), px(y as f32)),
278 size: size(px(width as f32), px(height as f32)),
279 }),
280 }
281 }
282}
283
284const DEFAULT_DOCK_STATE_KEY: &str = "default_dock_state";
285
286pub fn read_default_dock_state() -> Option<DockStructure> {
287 let json_str = KEY_VALUE_STORE
288 .read_kvp(DEFAULT_DOCK_STATE_KEY)
289 .log_err()
290 .flatten()?;
291
292 serde_json::from_str::<DockStructure>(&json_str).ok()
293}
294
295pub async fn write_default_dock_state(docks: DockStructure) -> anyhow::Result<()> {
296 let json_str = serde_json::to_string(&docks)?;
297 KEY_VALUE_STORE
298 .write_kvp(DEFAULT_DOCK_STATE_KEY.to_string(), json_str)
299 .await?;
300 Ok(())
301}
302
303#[derive(Debug)]
304pub struct Breakpoint {
305 pub position: u32,
306 pub message: Option<Arc<str>>,
307 pub condition: Option<Arc<str>>,
308 pub hit_condition: Option<Arc<str>>,
309 pub state: BreakpointState,
310}
311
312/// Wrapper for DB type of a breakpoint
313struct BreakpointStateWrapper<'a>(Cow<'a, BreakpointState>);
314
315impl From<BreakpointState> for BreakpointStateWrapper<'static> {
316 fn from(kind: BreakpointState) -> Self {
317 BreakpointStateWrapper(Cow::Owned(kind))
318 }
319}
320
321impl StaticColumnCount for BreakpointStateWrapper<'_> {
322 fn column_count() -> usize {
323 1
324 }
325}
326
327impl Bind for BreakpointStateWrapper<'_> {
328 fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result<i32> {
329 statement.bind(&self.0.to_int(), start_index)
330 }
331}
332
333impl Column for BreakpointStateWrapper<'_> {
334 fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> {
335 let state = statement.column_int(start_index)?;
336
337 match state {
338 0 => Ok((BreakpointState::Enabled.into(), start_index + 1)),
339 1 => Ok((BreakpointState::Disabled.into(), start_index + 1)),
340 _ => anyhow::bail!("Invalid BreakpointState discriminant {state}"),
341 }
342 }
343}
344
345impl sqlez::bindable::StaticColumnCount for Breakpoint {
346 fn column_count() -> usize {
347 // Position, log message, condition message, and hit condition message
348 4 + BreakpointStateWrapper::column_count()
349 }
350}
351
352impl sqlez::bindable::Bind for Breakpoint {
353 fn bind(
354 &self,
355 statement: &sqlez::statement::Statement,
356 start_index: i32,
357 ) -> anyhow::Result<i32> {
358 let next_index = statement.bind(&self.position, start_index)?;
359 let next_index = statement.bind(&self.message, next_index)?;
360 let next_index = statement.bind(&self.condition, next_index)?;
361 let next_index = statement.bind(&self.hit_condition, next_index)?;
362 statement.bind(
363 &BreakpointStateWrapper(Cow::Borrowed(&self.state)),
364 next_index,
365 )
366 }
367}
368
369impl Column for Breakpoint {
370 fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
371 let position = statement
372 .column_int(start_index)
373 .with_context(|| format!("Failed to read BreakPoint at index {start_index}"))?
374 as u32;
375 let (message, next_index) = Option::<String>::column(statement, start_index + 1)?;
376 let (condition, next_index) = Option::<String>::column(statement, next_index)?;
377 let (hit_condition, next_index) = Option::<String>::column(statement, next_index)?;
378 let (state, next_index) = BreakpointStateWrapper::column(statement, next_index)?;
379
380 Ok((
381 Breakpoint {
382 position,
383 message: message.map(Arc::from),
384 condition: condition.map(Arc::from),
385 hit_condition: hit_condition.map(Arc::from),
386 state: state.0.into_owned(),
387 },
388 next_index,
389 ))
390 }
391}
392
393#[derive(Clone, Debug, PartialEq)]
394struct SerializedPixels(gpui::Pixels);
395impl sqlez::bindable::StaticColumnCount for SerializedPixels {}
396
397impl sqlez::bindable::Bind for SerializedPixels {
398 fn bind(
399 &self,
400 statement: &sqlez::statement::Statement,
401 start_index: i32,
402 ) -> anyhow::Result<i32> {
403 let this: i32 = u32::from(self.0) as _;
404 this.bind(statement, start_index)
405 }
406}
407
408pub struct WorkspaceDb(ThreadSafeConnection);
409
410impl Domain for WorkspaceDb {
411 const NAME: &str = stringify!(WorkspaceDb);
412
413 const MIGRATIONS: &[&str] = &[
414 sql!(
415 CREATE TABLE workspaces(
416 workspace_id INTEGER PRIMARY KEY,
417 workspace_location BLOB UNIQUE,
418 dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
419 dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
420 dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed.
421 left_sidebar_open INTEGER, // Boolean
422 timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
423 FOREIGN KEY(dock_pane) REFERENCES panes(pane_id)
424 ) STRICT;
425
426 CREATE TABLE pane_groups(
427 group_id INTEGER PRIMARY KEY,
428 workspace_id INTEGER NOT NULL,
429 parent_group_id INTEGER, // NULL indicates that this is a root node
430 position INTEGER, // NULL indicates that this is a root node
431 axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal'
432 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
433 ON DELETE CASCADE
434 ON UPDATE CASCADE,
435 FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
436 ) STRICT;
437
438 CREATE TABLE panes(
439 pane_id INTEGER PRIMARY KEY,
440 workspace_id INTEGER NOT NULL,
441 active INTEGER NOT NULL, // Boolean
442 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
443 ON DELETE CASCADE
444 ON UPDATE CASCADE
445 ) STRICT;
446
447 CREATE TABLE center_panes(
448 pane_id INTEGER PRIMARY KEY,
449 parent_group_id INTEGER, // NULL means that this is a root pane
450 position INTEGER, // NULL means that this is a root pane
451 FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
452 ON DELETE CASCADE,
453 FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
454 ) STRICT;
455
456 CREATE TABLE items(
457 item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique
458 workspace_id INTEGER NOT NULL,
459 pane_id INTEGER NOT NULL,
460 kind TEXT NOT NULL,
461 position INTEGER NOT NULL,
462 active INTEGER NOT NULL,
463 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
464 ON DELETE CASCADE
465 ON UPDATE CASCADE,
466 FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
467 ON DELETE CASCADE,
468 PRIMARY KEY(item_id, workspace_id)
469 ) STRICT;
470 ),
471 sql!(
472 ALTER TABLE workspaces ADD COLUMN window_state TEXT;
473 ALTER TABLE workspaces ADD COLUMN window_x REAL;
474 ALTER TABLE workspaces ADD COLUMN window_y REAL;
475 ALTER TABLE workspaces ADD COLUMN window_width REAL;
476 ALTER TABLE workspaces ADD COLUMN window_height REAL;
477 ALTER TABLE workspaces ADD COLUMN display BLOB;
478 ),
479 // Drop foreign key constraint from workspaces.dock_pane to panes table.
480 sql!(
481 CREATE TABLE workspaces_2(
482 workspace_id INTEGER PRIMARY KEY,
483 workspace_location BLOB UNIQUE,
484 dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
485 dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
486 dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed.
487 left_sidebar_open INTEGER, // Boolean
488 timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
489 window_state TEXT,
490 window_x REAL,
491 window_y REAL,
492 window_width REAL,
493 window_height REAL,
494 display BLOB
495 ) STRICT;
496 INSERT INTO workspaces_2 SELECT * FROM workspaces;
497 DROP TABLE workspaces;
498 ALTER TABLE workspaces_2 RENAME TO workspaces;
499 ),
500 // Add panels related information
501 sql!(
502 ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool
503 ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT;
504 ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool
505 ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT;
506 ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool
507 ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT;
508 ),
509 // Add panel zoom persistence
510 sql!(
511 ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool
512 ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool
513 ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool
514 ),
515 // Add pane group flex data
516 sql!(
517 ALTER TABLE pane_groups ADD COLUMN flexes TEXT;
518 ),
519 // Add fullscreen field to workspace
520 // Deprecated, `WindowBounds` holds the fullscreen state now.
521 // Preserving so users can downgrade Zed.
522 sql!(
523 ALTER TABLE workspaces ADD COLUMN fullscreen INTEGER; //bool
524 ),
525 // Add preview field to items
526 sql!(
527 ALTER TABLE items ADD COLUMN preview INTEGER; //bool
528 ),
529 // Add centered_layout field to workspace
530 sql!(
531 ALTER TABLE workspaces ADD COLUMN centered_layout INTEGER; //bool
532 ),
533 sql!(
534 CREATE TABLE remote_projects (
535 remote_project_id INTEGER NOT NULL UNIQUE,
536 path TEXT,
537 dev_server_name TEXT
538 );
539 ALTER TABLE workspaces ADD COLUMN remote_project_id INTEGER;
540 ALTER TABLE workspaces RENAME COLUMN workspace_location TO local_paths;
541 ),
542 sql!(
543 DROP TABLE remote_projects;
544 CREATE TABLE dev_server_projects (
545 id INTEGER NOT NULL UNIQUE,
546 path TEXT,
547 dev_server_name TEXT
548 );
549 ALTER TABLE workspaces DROP COLUMN remote_project_id;
550 ALTER TABLE workspaces ADD COLUMN dev_server_project_id INTEGER;
551 ),
552 sql!(
553 ALTER TABLE workspaces ADD COLUMN local_paths_order BLOB;
554 ),
555 sql!(
556 ALTER TABLE workspaces ADD COLUMN session_id TEXT DEFAULT NULL;
557 ),
558 sql!(
559 ALTER TABLE workspaces ADD COLUMN window_id INTEGER DEFAULT NULL;
560 ),
561 sql!(
562 ALTER TABLE panes ADD COLUMN pinned_count INTEGER DEFAULT 0;
563 ),
564 sql!(
565 CREATE TABLE ssh_projects (
566 id INTEGER PRIMARY KEY,
567 host TEXT NOT NULL,
568 port INTEGER,
569 path TEXT NOT NULL,
570 user TEXT
571 );
572 ALTER TABLE workspaces ADD COLUMN ssh_project_id INTEGER REFERENCES ssh_projects(id) ON DELETE CASCADE;
573 ),
574 sql!(
575 ALTER TABLE ssh_projects RENAME COLUMN path TO paths;
576 ),
577 sql!(
578 CREATE TABLE toolchains (
579 workspace_id INTEGER,
580 worktree_id INTEGER,
581 language_name TEXT NOT NULL,
582 name TEXT NOT NULL,
583 path TEXT NOT NULL,
584 PRIMARY KEY (workspace_id, worktree_id, language_name)
585 );
586 ),
587 sql!(
588 ALTER TABLE toolchains ADD COLUMN raw_json TEXT DEFAULT "{}";
589 ),
590 sql!(
591 CREATE TABLE breakpoints (
592 workspace_id INTEGER NOT NULL,
593 path TEXT NOT NULL,
594 breakpoint_location INTEGER NOT NULL,
595 kind INTEGER NOT NULL,
596 log_message TEXT,
597 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
598 ON DELETE CASCADE
599 ON UPDATE CASCADE
600 );
601 ),
602 sql!(
603 ALTER TABLE workspaces ADD COLUMN local_paths_array TEXT;
604 CREATE UNIQUE INDEX local_paths_array_uq ON workspaces(local_paths_array);
605 ALTER TABLE workspaces ADD COLUMN local_paths_order_array TEXT;
606 ),
607 sql!(
608 ALTER TABLE breakpoints ADD COLUMN state INTEGER DEFAULT(0) NOT NULL
609 ),
610 sql!(
611 ALTER TABLE breakpoints DROP COLUMN kind
612 ),
613 sql!(ALTER TABLE toolchains ADD COLUMN relative_worktree_path TEXT DEFAULT "" NOT NULL),
614 sql!(
615 ALTER TABLE breakpoints ADD COLUMN condition TEXT;
616 ALTER TABLE breakpoints ADD COLUMN hit_condition TEXT;
617 ),
618 sql!(CREATE TABLE toolchains2 (
619 workspace_id INTEGER,
620 worktree_id INTEGER,
621 language_name TEXT NOT NULL,
622 name TEXT NOT NULL,
623 path TEXT NOT NULL,
624 raw_json TEXT NOT NULL,
625 relative_worktree_path TEXT NOT NULL,
626 PRIMARY KEY (workspace_id, worktree_id, language_name, relative_worktree_path)) STRICT;
627 INSERT INTO toolchains2
628 SELECT * FROM toolchains;
629 DROP TABLE toolchains;
630 ALTER TABLE toolchains2 RENAME TO toolchains;
631 ),
632 sql!(
633 CREATE TABLE ssh_connections (
634 id INTEGER PRIMARY KEY,
635 host TEXT NOT NULL,
636 port INTEGER,
637 user TEXT
638 );
639
640 INSERT INTO ssh_connections (host, port, user)
641 SELECT DISTINCT host, port, user
642 FROM ssh_projects;
643
644 CREATE TABLE workspaces_2(
645 workspace_id INTEGER PRIMARY KEY,
646 paths TEXT,
647 paths_order TEXT,
648 ssh_connection_id INTEGER REFERENCES ssh_connections(id),
649 timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
650 window_state TEXT,
651 window_x REAL,
652 window_y REAL,
653 window_width REAL,
654 window_height REAL,
655 display BLOB,
656 left_dock_visible INTEGER,
657 left_dock_active_panel TEXT,
658 right_dock_visible INTEGER,
659 right_dock_active_panel TEXT,
660 bottom_dock_visible INTEGER,
661 bottom_dock_active_panel TEXT,
662 left_dock_zoom INTEGER,
663 right_dock_zoom INTEGER,
664 bottom_dock_zoom INTEGER,
665 fullscreen INTEGER,
666 centered_layout INTEGER,
667 session_id TEXT,
668 window_id INTEGER
669 ) STRICT;
670
671 INSERT
672 INTO workspaces_2
673 SELECT
674 workspaces.workspace_id,
675 CASE
676 WHEN ssh_projects.id IS NOT NULL THEN ssh_projects.paths
677 ELSE
678 CASE
679 WHEN workspaces.local_paths_array IS NULL OR workspaces.local_paths_array = "" THEN
680 NULL
681 ELSE
682 replace(workspaces.local_paths_array, ',', CHAR(10))
683 END
684 END as paths,
685
686 CASE
687 WHEN ssh_projects.id IS NOT NULL THEN ""
688 ELSE workspaces.local_paths_order_array
689 END as paths_order,
690
691 CASE
692 WHEN ssh_projects.id IS NOT NULL THEN (
693 SELECT ssh_connections.id
694 FROM ssh_connections
695 WHERE
696 ssh_connections.host IS ssh_projects.host AND
697 ssh_connections.port IS ssh_projects.port AND
698 ssh_connections.user IS ssh_projects.user
699 )
700 ELSE NULL
701 END as ssh_connection_id,
702
703 workspaces.timestamp,
704 workspaces.window_state,
705 workspaces.window_x,
706 workspaces.window_y,
707 workspaces.window_width,
708 workspaces.window_height,
709 workspaces.display,
710 workspaces.left_dock_visible,
711 workspaces.left_dock_active_panel,
712 workspaces.right_dock_visible,
713 workspaces.right_dock_active_panel,
714 workspaces.bottom_dock_visible,
715 workspaces.bottom_dock_active_panel,
716 workspaces.left_dock_zoom,
717 workspaces.right_dock_zoom,
718 workspaces.bottom_dock_zoom,
719 workspaces.fullscreen,
720 workspaces.centered_layout,
721 workspaces.session_id,
722 workspaces.window_id
723 FROM
724 workspaces LEFT JOIN
725 ssh_projects ON
726 workspaces.ssh_project_id = ssh_projects.id;
727
728 DELETE FROM workspaces_2
729 WHERE workspace_id NOT IN (
730 SELECT MAX(workspace_id)
731 FROM workspaces_2
732 GROUP BY ssh_connection_id, paths
733 );
734
735 DROP TABLE ssh_projects;
736 DROP TABLE workspaces;
737 ALTER TABLE workspaces_2 RENAME TO workspaces;
738
739 CREATE UNIQUE INDEX ix_workspaces_location ON workspaces(ssh_connection_id, paths);
740 ),
741 // Fix any data from when workspaces.paths were briefly encoded as JSON arrays
742 sql!(
743 UPDATE workspaces
744 SET paths = CASE
745 WHEN substr(paths, 1, 2) = '[' || '"' AND substr(paths, -2, 2) = '"' || ']' THEN
746 replace(
747 substr(paths, 3, length(paths) - 4),
748 '"' || ',' || '"',
749 CHAR(10)
750 )
751 ELSE
752 replace(paths, ',', CHAR(10))
753 END
754 WHERE paths IS NOT NULL
755 ),
756 sql!(
757 CREATE TABLE remote_connections(
758 id INTEGER PRIMARY KEY,
759 kind TEXT NOT NULL,
760 host TEXT,
761 port INTEGER,
762 user TEXT,
763 distro TEXT
764 );
765
766 CREATE TABLE workspaces_2(
767 workspace_id INTEGER PRIMARY KEY,
768 paths TEXT,
769 paths_order TEXT,
770 remote_connection_id INTEGER REFERENCES remote_connections(id),
771 timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
772 window_state TEXT,
773 window_x REAL,
774 window_y REAL,
775 window_width REAL,
776 window_height REAL,
777 display BLOB,
778 left_dock_visible INTEGER,
779 left_dock_active_panel TEXT,
780 right_dock_visible INTEGER,
781 right_dock_active_panel TEXT,
782 bottom_dock_visible INTEGER,
783 bottom_dock_active_panel TEXT,
784 left_dock_zoom INTEGER,
785 right_dock_zoom INTEGER,
786 bottom_dock_zoom INTEGER,
787 fullscreen INTEGER,
788 centered_layout INTEGER,
789 session_id TEXT,
790 window_id INTEGER
791 ) STRICT;
792
793 INSERT INTO remote_connections
794 SELECT
795 id,
796 "ssh" as kind,
797 host,
798 port,
799 user,
800 NULL as distro
801 FROM ssh_connections;
802
803 INSERT
804 INTO workspaces_2
805 SELECT
806 workspace_id,
807 paths,
808 paths_order,
809 ssh_connection_id as remote_connection_id,
810 timestamp,
811 window_state,
812 window_x,
813 window_y,
814 window_width,
815 window_height,
816 display,
817 left_dock_visible,
818 left_dock_active_panel,
819 right_dock_visible,
820 right_dock_active_panel,
821 bottom_dock_visible,
822 bottom_dock_active_panel,
823 left_dock_zoom,
824 right_dock_zoom,
825 bottom_dock_zoom,
826 fullscreen,
827 centered_layout,
828 session_id,
829 window_id
830 FROM
831 workspaces;
832
833 DROP TABLE workspaces;
834 ALTER TABLE workspaces_2 RENAME TO workspaces;
835
836 CREATE UNIQUE INDEX ix_workspaces_location ON workspaces(remote_connection_id, paths);
837 ),
838 sql!(CREATE TABLE user_toolchains (
839 remote_connection_id INTEGER,
840 workspace_id INTEGER NOT NULL,
841 worktree_id INTEGER NOT NULL,
842 relative_worktree_path TEXT NOT NULL,
843 language_name TEXT NOT NULL,
844 name TEXT NOT NULL,
845 path TEXT NOT NULL,
846 raw_json TEXT NOT NULL,
847
848 PRIMARY KEY (workspace_id, worktree_id, relative_worktree_path, language_name, name, path, raw_json)
849 ) STRICT;),
850 sql!(
851 DROP TABLE ssh_connections;
852 ),
853 sql!(
854 ALTER TABLE remote_connections ADD COLUMN name TEXT;
855 ALTER TABLE remote_connections ADD COLUMN container_id TEXT;
856 ),
857 sql!(
858 CREATE TABLE IF NOT EXISTS trusted_worktrees (
859 trust_id INTEGER PRIMARY KEY AUTOINCREMENT,
860 absolute_path TEXT,
861 user_name TEXT,
862 host_name TEXT
863 ) STRICT;
864 ),
865 sql!(CREATE TABLE toolchains2 (
866 workspace_id INTEGER,
867 worktree_root_path TEXT NOT NULL,
868 language_name TEXT NOT NULL,
869 name TEXT NOT NULL,
870 path TEXT NOT NULL,
871 raw_json TEXT NOT NULL,
872 relative_worktree_path TEXT NOT NULL,
873 PRIMARY KEY (workspace_id, worktree_root_path, language_name, relative_worktree_path)) STRICT;
874 INSERT OR REPLACE INTO toolchains2
875 // The `instr(paths, '\n') = 0` part allows us to find all
876 // workspaces that have a single worktree, as `\n` is used as a
877 // separator when serializing the workspace paths, so if no `\n` is
878 // found, we know we have a single worktree.
879 SELECT toolchains.workspace_id, paths, language_name, name, path, raw_json, relative_worktree_path FROM toolchains INNER JOIN workspaces ON toolchains.workspace_id = workspaces.workspace_id AND instr(paths, '\n') = 0;
880 DROP TABLE toolchains;
881 ALTER TABLE toolchains2 RENAME TO toolchains;
882 ),
883 sql!(CREATE TABLE user_toolchains2 (
884 remote_connection_id INTEGER,
885 workspace_id INTEGER NOT NULL,
886 worktree_root_path TEXT NOT NULL,
887 relative_worktree_path TEXT NOT NULL,
888 language_name TEXT NOT NULL,
889 name TEXT NOT NULL,
890 path TEXT NOT NULL,
891 raw_json TEXT NOT NULL,
892
893 PRIMARY KEY (workspace_id, worktree_root_path, relative_worktree_path, language_name, name, path, raw_json)) STRICT;
894 INSERT OR REPLACE INTO user_toolchains2
895 // The `instr(paths, '\n') = 0` part allows us to find all
896 // workspaces that have a single worktree, as `\n` is used as a
897 // separator when serializing the workspace paths, so if no `\n` is
898 // found, we know we have a single worktree.
899 SELECT user_toolchains.remote_connection_id, user_toolchains.workspace_id, paths, relative_worktree_path, language_name, name, path, raw_json FROM user_toolchains INNER JOIN workspaces ON user_toolchains.workspace_id = workspaces.workspace_id AND instr(paths, '\n') = 0;
900 DROP TABLE user_toolchains;
901 ALTER TABLE user_toolchains2 RENAME TO user_toolchains;
902 ),
903 sql!(
904 ALTER TABLE remote_connections ADD COLUMN use_podman BOOLEAN;
905 ),
906 ];
907
908 // Allow recovering from bad migration that was initially shipped to nightly
909 // when introducing the ssh_connections table.
910 fn should_allow_migration_change(_index: usize, old: &str, new: &str) -> bool {
911 old.starts_with("CREATE TABLE ssh_connections")
912 && new.starts_with("CREATE TABLE ssh_connections")
913 }
914}
915
916db::static_connection!(DB, WorkspaceDb, []);
917
918impl WorkspaceDb {
919 /// Returns a serialized workspace for the given worktree_roots. If the passed array
920 /// is empty, the most recent workspace is returned instead. If no workspace for the
921 /// passed roots is stored, returns none.
922 pub(crate) fn workspace_for_roots<P: AsRef<Path>>(
923 &self,
924 worktree_roots: &[P],
925 ) -> Option<SerializedWorkspace> {
926 self.workspace_for_roots_internal(worktree_roots, None)
927 }
928
929 pub(crate) fn remote_workspace_for_roots<P: AsRef<Path>>(
930 &self,
931 worktree_roots: &[P],
932 remote_project_id: RemoteConnectionId,
933 ) -> Option<SerializedWorkspace> {
934 self.workspace_for_roots_internal(worktree_roots, Some(remote_project_id))
935 }
936
937 pub(crate) fn workspace_for_roots_internal<P: AsRef<Path>>(
938 &self,
939 worktree_roots: &[P],
940 remote_connection_id: Option<RemoteConnectionId>,
941 ) -> Option<SerializedWorkspace> {
942 // paths are sorted before db interactions to ensure that the order of the paths
943 // doesn't affect the workspace selection for existing workspaces
944 let root_paths = PathList::new(worktree_roots);
945
946 // Empty workspaces cannot be matched by paths (all empty workspaces have paths = "").
947 // They should only be restored via workspace_for_id during session restoration.
948 if root_paths.is_empty() && remote_connection_id.is_none() {
949 return None;
950 }
951
952 // Note that we re-assign the workspace_id here in case it's empty
953 // and we've grabbed the most recent workspace
954 let (
955 workspace_id,
956 paths,
957 paths_order,
958 window_bounds,
959 display,
960 centered_layout,
961 docks,
962 window_id,
963 ): (
964 WorkspaceId,
965 String,
966 String,
967 Option<SerializedWindowBounds>,
968 Option<Uuid>,
969 Option<bool>,
970 DockStructure,
971 Option<u64>,
972 ) = self
973 .select_row_bound(sql! {
974 SELECT
975 workspace_id,
976 paths,
977 paths_order,
978 window_state,
979 window_x,
980 window_y,
981 window_width,
982 window_height,
983 display,
984 centered_layout,
985 left_dock_visible,
986 left_dock_active_panel,
987 left_dock_zoom,
988 right_dock_visible,
989 right_dock_active_panel,
990 right_dock_zoom,
991 bottom_dock_visible,
992 bottom_dock_active_panel,
993 bottom_dock_zoom,
994 window_id
995 FROM workspaces
996 WHERE
997 paths IS ? AND
998 remote_connection_id IS ?
999 LIMIT 1
1000 })
1001 .and_then(|mut prepared_statement| {
1002 (prepared_statement)((
1003 root_paths.serialize().paths,
1004 remote_connection_id.map(|id| id.0 as i32),
1005 ))
1006 })
1007 .context("No workspaces found")
1008 .warn_on_err()
1009 .flatten()?;
1010
1011 let paths = PathList::deserialize(&SerializedPathList {
1012 paths,
1013 order: paths_order,
1014 });
1015
1016 let remote_connection_options = if let Some(remote_connection_id) = remote_connection_id {
1017 self.remote_connection(remote_connection_id)
1018 .context("Get remote connection")
1019 .log_err()
1020 } else {
1021 None
1022 };
1023
1024 Some(SerializedWorkspace {
1025 id: workspace_id,
1026 location: match remote_connection_options {
1027 Some(options) => SerializedWorkspaceLocation::Remote(options),
1028 None => SerializedWorkspaceLocation::Local,
1029 },
1030 paths,
1031 center_group: self
1032 .get_center_pane_group(workspace_id)
1033 .context("Getting center group")
1034 .log_err()?,
1035 window_bounds,
1036 centered_layout: centered_layout.unwrap_or(false),
1037 display,
1038 docks,
1039 session_id: None,
1040 breakpoints: self.breakpoints(workspace_id),
1041 window_id,
1042 user_toolchains: self.user_toolchains(workspace_id, remote_connection_id),
1043 })
1044 }
1045
1046 /// Returns the workspace with the given ID, loading all associated data.
1047 pub(crate) fn workspace_for_id(
1048 &self,
1049 workspace_id: WorkspaceId,
1050 ) -> Option<SerializedWorkspace> {
1051 let (
1052 paths,
1053 paths_order,
1054 window_bounds,
1055 display,
1056 centered_layout,
1057 docks,
1058 window_id,
1059 remote_connection_id,
1060 ): (
1061 String,
1062 String,
1063 Option<SerializedWindowBounds>,
1064 Option<Uuid>,
1065 Option<bool>,
1066 DockStructure,
1067 Option<u64>,
1068 Option<i32>,
1069 ) = self
1070 .select_row_bound(sql! {
1071 SELECT
1072 paths,
1073 paths_order,
1074 window_state,
1075 window_x,
1076 window_y,
1077 window_width,
1078 window_height,
1079 display,
1080 centered_layout,
1081 left_dock_visible,
1082 left_dock_active_panel,
1083 left_dock_zoom,
1084 right_dock_visible,
1085 right_dock_active_panel,
1086 right_dock_zoom,
1087 bottom_dock_visible,
1088 bottom_dock_active_panel,
1089 bottom_dock_zoom,
1090 window_id,
1091 remote_connection_id
1092 FROM workspaces
1093 WHERE workspace_id = ?
1094 })
1095 .and_then(|mut prepared_statement| (prepared_statement)(workspace_id))
1096 .context("No workspace found for id")
1097 .warn_on_err()
1098 .flatten()?;
1099
1100 let paths = PathList::deserialize(&SerializedPathList {
1101 paths,
1102 order: paths_order,
1103 });
1104
1105 let remote_connection_id = remote_connection_id.map(|id| RemoteConnectionId(id as u64));
1106 let remote_connection_options = if let Some(remote_connection_id) = remote_connection_id {
1107 self.remote_connection(remote_connection_id)
1108 .context("Get remote connection")
1109 .log_err()
1110 } else {
1111 None
1112 };
1113
1114 Some(SerializedWorkspace {
1115 id: workspace_id,
1116 location: match remote_connection_options {
1117 Some(options) => SerializedWorkspaceLocation::Remote(options),
1118 None => SerializedWorkspaceLocation::Local,
1119 },
1120 paths,
1121 center_group: self
1122 .get_center_pane_group(workspace_id)
1123 .context("Getting center group")
1124 .log_err()?,
1125 window_bounds,
1126 centered_layout: centered_layout.unwrap_or(false),
1127 display,
1128 docks,
1129 session_id: None,
1130 breakpoints: self.breakpoints(workspace_id),
1131 window_id,
1132 user_toolchains: self.user_toolchains(workspace_id, remote_connection_id),
1133 })
1134 }
1135
1136 fn breakpoints(&self, workspace_id: WorkspaceId) -> BTreeMap<Arc<Path>, Vec<SourceBreakpoint>> {
1137 let breakpoints: Result<Vec<(PathBuf, Breakpoint)>> = self
1138 .select_bound(sql! {
1139 SELECT path, breakpoint_location, log_message, condition, hit_condition, state
1140 FROM breakpoints
1141 WHERE workspace_id = ?
1142 })
1143 .and_then(|mut prepared_statement| (prepared_statement)(workspace_id));
1144
1145 match breakpoints {
1146 Ok(bp) => {
1147 if bp.is_empty() {
1148 log::debug!("Breakpoints are empty after querying database for them");
1149 }
1150
1151 let mut map: BTreeMap<Arc<Path>, Vec<SourceBreakpoint>> = Default::default();
1152
1153 for (path, breakpoint) in bp {
1154 let path: Arc<Path> = path.into();
1155 map.entry(path.clone()).or_default().push(SourceBreakpoint {
1156 row: breakpoint.position,
1157 path,
1158 message: breakpoint.message,
1159 condition: breakpoint.condition,
1160 hit_condition: breakpoint.hit_condition,
1161 state: breakpoint.state,
1162 });
1163 }
1164
1165 for (path, bps) in map.iter() {
1166 log::info!(
1167 "Got {} breakpoints from database at path: {}",
1168 bps.len(),
1169 path.to_string_lossy()
1170 );
1171 }
1172
1173 map
1174 }
1175 Err(msg) => {
1176 log::error!("Breakpoints query failed with msg: {msg}");
1177 Default::default()
1178 }
1179 }
1180 }
1181
1182 fn user_toolchains(
1183 &self,
1184 workspace_id: WorkspaceId,
1185 remote_connection_id: Option<RemoteConnectionId>,
1186 ) -> BTreeMap<ToolchainScope, IndexSet<Toolchain>> {
1187 type RowKind = (WorkspaceId, String, String, String, String, String, String);
1188
1189 let toolchains: Vec<RowKind> = self
1190 .select_bound(sql! {
1191 SELECT workspace_id, worktree_root_path, relative_worktree_path,
1192 language_name, name, path, raw_json
1193 FROM user_toolchains WHERE remote_connection_id IS ?1 AND (
1194 workspace_id IN (0, ?2)
1195 )
1196 })
1197 .and_then(|mut statement| {
1198 (statement)((remote_connection_id.map(|id| id.0), workspace_id))
1199 })
1200 .unwrap_or_default();
1201 let mut ret = BTreeMap::<_, IndexSet<_>>::default();
1202
1203 for (
1204 _workspace_id,
1205 worktree_root_path,
1206 relative_worktree_path,
1207 language_name,
1208 name,
1209 path,
1210 raw_json,
1211 ) in toolchains
1212 {
1213 // INTEGER's that are primary keys (like workspace ids, remote connection ids and such) start at 1, so we're safe to
1214 let scope = if _workspace_id == WorkspaceId(0) {
1215 debug_assert_eq!(worktree_root_path, String::default());
1216 debug_assert_eq!(relative_worktree_path, String::default());
1217 ToolchainScope::Global
1218 } else {
1219 debug_assert_eq!(workspace_id, _workspace_id);
1220 debug_assert_eq!(
1221 worktree_root_path == String::default(),
1222 relative_worktree_path == String::default()
1223 );
1224
1225 let Some(relative_path) = RelPath::unix(&relative_worktree_path).log_err() else {
1226 continue;
1227 };
1228 if worktree_root_path != String::default()
1229 && relative_worktree_path != String::default()
1230 {
1231 ToolchainScope::Subproject(
1232 Arc::from(worktree_root_path.as_ref()),
1233 relative_path.into(),
1234 )
1235 } else {
1236 ToolchainScope::Project
1237 }
1238 };
1239 let Ok(as_json) = serde_json::from_str(&raw_json) else {
1240 continue;
1241 };
1242 let toolchain = Toolchain {
1243 name: SharedString::from(name),
1244 path: SharedString::from(path),
1245 language_name: LanguageName::from_proto(language_name),
1246 as_json,
1247 };
1248 ret.entry(scope).or_default().insert(toolchain);
1249 }
1250
1251 ret
1252 }
1253
1254 /// Saves a workspace using the worktree roots. Will garbage collect any workspaces
1255 /// that used this workspace previously
1256 pub(crate) async fn save_workspace(&self, workspace: SerializedWorkspace) {
1257 let paths = workspace.paths.serialize();
1258 log::debug!("Saving workspace at location: {:?}", workspace.location);
1259 self.write(move |conn| {
1260 conn.with_savepoint("update_worktrees", || {
1261 let remote_connection_id = match workspace.location.clone() {
1262 SerializedWorkspaceLocation::Local => None,
1263 SerializedWorkspaceLocation::Remote(connection_options) => {
1264 Some(Self::get_or_create_remote_connection_internal(
1265 conn,
1266 connection_options
1267 )?.0)
1268 }
1269 };
1270
1271 // Clear out panes and pane_groups
1272 conn.exec_bound(sql!(
1273 DELETE FROM pane_groups WHERE workspace_id = ?1;
1274 DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)
1275 .context("Clearing old panes")?;
1276
1277 conn.exec_bound(
1278 sql!(
1279 DELETE FROM breakpoints WHERE workspace_id = ?1;
1280 )
1281 )?(workspace.id).context("Clearing old breakpoints")?;
1282
1283 for (path, breakpoints) in workspace.breakpoints {
1284 for bp in breakpoints {
1285 let state = BreakpointStateWrapper::from(bp.state);
1286 match conn.exec_bound(sql!(
1287 INSERT INTO breakpoints (workspace_id, path, breakpoint_location, log_message, condition, hit_condition, state)
1288 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7);))?
1289
1290 ((
1291 workspace.id,
1292 path.as_ref(),
1293 bp.row,
1294 bp.message,
1295 bp.condition,
1296 bp.hit_condition,
1297 state,
1298 )) {
1299 Ok(_) => {
1300 log::debug!("Stored breakpoint at row: {} in path: {}", bp.row, path.to_string_lossy())
1301 }
1302 Err(err) => {
1303 log::error!("{err}");
1304 continue;
1305 }
1306 }
1307 }
1308 }
1309
1310 conn.exec_bound(
1311 sql!(
1312 DELETE FROM user_toolchains WHERE workspace_id = ?1;
1313 )
1314 )?(workspace.id).context("Clearing old user toolchains")?;
1315
1316 for (scope, toolchains) in workspace.user_toolchains {
1317 for toolchain in toolchains {
1318 let query = sql!(INSERT OR REPLACE INTO user_toolchains(remote_connection_id, workspace_id, worktree_root_path, relative_worktree_path, language_name, name, path, raw_json) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8));
1319 let (workspace_id, worktree_root_path, relative_worktree_path) = match scope {
1320 ToolchainScope::Subproject(ref worktree_root_path, ref path) => (Some(workspace.id), Some(worktree_root_path.to_string_lossy().into_owned()), Some(path.as_unix_str().to_owned())),
1321 ToolchainScope::Project => (Some(workspace.id), None, None),
1322 ToolchainScope::Global => (None, None, None),
1323 };
1324 let args = (remote_connection_id, workspace_id.unwrap_or(WorkspaceId(0)), worktree_root_path.unwrap_or_default(), relative_worktree_path.unwrap_or_default(),
1325 toolchain.language_name.as_ref().to_owned(), toolchain.name.to_string(), toolchain.path.to_string(), toolchain.as_json.to_string());
1326 if let Err(err) = conn.exec_bound(query)?(args) {
1327 log::error!("{err}");
1328 continue;
1329 }
1330 }
1331 }
1332
1333 // Clear out old workspaces with the same paths.
1334 // Skip this for empty workspaces - they are identified by workspace_id, not paths.
1335 // Multiple empty workspaces with different content should coexist.
1336 if !paths.paths.is_empty() {
1337 conn.exec_bound(sql!(
1338 DELETE
1339 FROM workspaces
1340 WHERE
1341 workspace_id != ?1 AND
1342 paths IS ?2 AND
1343 remote_connection_id IS ?3
1344 ))?((
1345 workspace.id,
1346 paths.paths.clone(),
1347 remote_connection_id,
1348 ))
1349 .context("clearing out old locations")?;
1350 }
1351
1352 // Upsert
1353 let query = sql!(
1354 INSERT INTO workspaces(
1355 workspace_id,
1356 paths,
1357 paths_order,
1358 remote_connection_id,
1359 left_dock_visible,
1360 left_dock_active_panel,
1361 left_dock_zoom,
1362 right_dock_visible,
1363 right_dock_active_panel,
1364 right_dock_zoom,
1365 bottom_dock_visible,
1366 bottom_dock_active_panel,
1367 bottom_dock_zoom,
1368 session_id,
1369 window_id,
1370 timestamp
1371 )
1372 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, CURRENT_TIMESTAMP)
1373 ON CONFLICT DO
1374 UPDATE SET
1375 paths = ?2,
1376 paths_order = ?3,
1377 remote_connection_id = ?4,
1378 left_dock_visible = ?5,
1379 left_dock_active_panel = ?6,
1380 left_dock_zoom = ?7,
1381 right_dock_visible = ?8,
1382 right_dock_active_panel = ?9,
1383 right_dock_zoom = ?10,
1384 bottom_dock_visible = ?11,
1385 bottom_dock_active_panel = ?12,
1386 bottom_dock_zoom = ?13,
1387 session_id = ?14,
1388 window_id = ?15,
1389 timestamp = CURRENT_TIMESTAMP
1390 );
1391 let mut prepared_query = conn.exec_bound(query)?;
1392 let args = (
1393 workspace.id,
1394 paths.paths.clone(),
1395 paths.order.clone(),
1396 remote_connection_id,
1397 workspace.docks,
1398 workspace.session_id,
1399 workspace.window_id,
1400 );
1401
1402 prepared_query(args).context("Updating workspace")?;
1403
1404 // Save center pane group
1405 Self::save_pane_group(conn, workspace.id, &workspace.center_group, None)
1406 .context("save pane group in save workspace")?;
1407
1408 Ok(())
1409 })
1410 .log_err();
1411 })
1412 .await;
1413 }
1414
1415 pub(crate) async fn get_or_create_remote_connection(
1416 &self,
1417 options: RemoteConnectionOptions,
1418 ) -> Result<RemoteConnectionId> {
1419 self.write(move |conn| Self::get_or_create_remote_connection_internal(conn, options))
1420 .await
1421 }
1422
1423 fn get_or_create_remote_connection_internal(
1424 this: &Connection,
1425 options: RemoteConnectionOptions,
1426 ) -> Result<RemoteConnectionId> {
1427 let kind;
1428 let user: Option<String>;
1429 let mut host = None;
1430 let mut port = None;
1431 let mut distro = None;
1432 let mut name = None;
1433 let mut container_id = None;
1434 let mut use_podman = None;
1435 match options {
1436 RemoteConnectionOptions::Ssh(options) => {
1437 kind = RemoteConnectionKind::Ssh;
1438 host = Some(options.host.to_string());
1439 port = options.port;
1440 user = options.username;
1441 }
1442 RemoteConnectionOptions::Wsl(options) => {
1443 kind = RemoteConnectionKind::Wsl;
1444 distro = Some(options.distro_name);
1445 user = options.user;
1446 }
1447 RemoteConnectionOptions::Docker(options) => {
1448 kind = RemoteConnectionKind::Docker;
1449 container_id = Some(options.container_id);
1450 name = Some(options.name);
1451 use_podman = Some(options.use_podman);
1452 user = Some(options.remote_user);
1453 }
1454 #[cfg(any(test, feature = "test-support"))]
1455 RemoteConnectionOptions::Mock(options) => {
1456 kind = RemoteConnectionKind::Ssh;
1457 host = Some(format!("mock-{}", options.id));
1458 user = Some(format!("mock-user-{}", options.id));
1459 }
1460 }
1461 Self::get_or_create_remote_connection_query(
1462 this,
1463 kind,
1464 host,
1465 port,
1466 user,
1467 distro,
1468 name,
1469 container_id,
1470 use_podman,
1471 )
1472 }
1473
1474 fn get_or_create_remote_connection_query(
1475 this: &Connection,
1476 kind: RemoteConnectionKind,
1477 host: Option<String>,
1478 port: Option<u16>,
1479 user: Option<String>,
1480 distro: Option<String>,
1481 name: Option<String>,
1482 container_id: Option<String>,
1483 use_podman: Option<bool>,
1484 ) -> Result<RemoteConnectionId> {
1485 if let Some(id) = this.select_row_bound(sql!(
1486 SELECT id
1487 FROM remote_connections
1488 WHERE
1489 kind IS ? AND
1490 host IS ? AND
1491 port IS ? AND
1492 user IS ? AND
1493 distro IS ? AND
1494 name IS ? AND
1495 container_id IS ?
1496 LIMIT 1
1497 ))?((
1498 kind.serialize(),
1499 host.clone(),
1500 port,
1501 user.clone(),
1502 distro.clone(),
1503 name.clone(),
1504 container_id.clone(),
1505 ))? {
1506 Ok(RemoteConnectionId(id))
1507 } else {
1508 let id = this.select_row_bound(sql!(
1509 INSERT INTO remote_connections (
1510 kind,
1511 host,
1512 port,
1513 user,
1514 distro,
1515 name,
1516 container_id,
1517 use_podman
1518 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
1519 RETURNING id
1520 ))?((
1521 kind.serialize(),
1522 host,
1523 port,
1524 user,
1525 distro,
1526 name,
1527 container_id,
1528 use_podman,
1529 ))?
1530 .context("failed to insert remote project")?;
1531 Ok(RemoteConnectionId(id))
1532 }
1533 }
1534
1535 query! {
1536 pub async fn next_id() -> Result<WorkspaceId> {
1537 INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id
1538 }
1539 }
1540
1541 fn recent_workspaces(
1542 &self,
1543 ) -> Result<Vec<(WorkspaceId, PathList, Option<RemoteConnectionId>)>> {
1544 Ok(self
1545 .recent_workspaces_query()?
1546 .into_iter()
1547 .map(|(id, paths, order, remote_connection_id)| {
1548 (
1549 id,
1550 PathList::deserialize(&SerializedPathList { paths, order }),
1551 remote_connection_id.map(RemoteConnectionId),
1552 )
1553 })
1554 .collect())
1555 }
1556
1557 query! {
1558 fn recent_workspaces_query() -> Result<Vec<(WorkspaceId, String, String, Option<u64>)>> {
1559 SELECT workspace_id, paths, paths_order, remote_connection_id
1560 FROM workspaces
1561 WHERE
1562 paths IS NOT NULL OR
1563 remote_connection_id IS NOT NULL
1564 ORDER BY timestamp DESC
1565 }
1566 }
1567
1568 fn session_workspaces(
1569 &self,
1570 session_id: String,
1571 ) -> Result<
1572 Vec<(
1573 WorkspaceId,
1574 PathList,
1575 Option<u64>,
1576 Option<RemoteConnectionId>,
1577 )>,
1578 > {
1579 Ok(self
1580 .session_workspaces_query(session_id)?
1581 .into_iter()
1582 .map(
1583 |(workspace_id, paths, order, window_id, remote_connection_id)| {
1584 (
1585 WorkspaceId(workspace_id),
1586 PathList::deserialize(&SerializedPathList { paths, order }),
1587 window_id,
1588 remote_connection_id.map(RemoteConnectionId),
1589 )
1590 },
1591 )
1592 .collect())
1593 }
1594
1595 query! {
1596 fn session_workspaces_query(session_id: String) -> Result<Vec<(i64, String, String, Option<u64>, Option<u64>)>> {
1597 SELECT workspace_id, paths, paths_order, window_id, remote_connection_id
1598 FROM workspaces
1599 WHERE session_id = ?1
1600 ORDER BY timestamp DESC
1601 }
1602 }
1603
1604 query! {
1605 pub fn breakpoints_for_file(workspace_id: WorkspaceId, file_path: &Path) -> Result<Vec<Breakpoint>> {
1606 SELECT breakpoint_location
1607 FROM breakpoints
1608 WHERE workspace_id= ?1 AND path = ?2
1609 }
1610 }
1611
1612 query! {
1613 pub fn clear_breakpoints(file_path: &Path) -> Result<()> {
1614 DELETE FROM breakpoints
1615 WHERE file_path = ?2
1616 }
1617 }
1618
1619 fn remote_connections(&self) -> Result<HashMap<RemoteConnectionId, RemoteConnectionOptions>> {
1620 Ok(self.select(sql!(
1621 SELECT
1622 id, kind, host, port, user, distro, container_id, name, use_podman
1623 FROM
1624 remote_connections
1625 ))?()?
1626 .into_iter()
1627 .filter_map(
1628 |(id, kind, host, port, user, distro, container_id, name, use_podman)| {
1629 Some((
1630 RemoteConnectionId(id),
1631 Self::remote_connection_from_row(
1632 kind,
1633 host,
1634 port,
1635 user,
1636 distro,
1637 container_id,
1638 name,
1639 use_podman,
1640 )?,
1641 ))
1642 },
1643 )
1644 .collect())
1645 }
1646
1647 pub(crate) fn remote_connection(
1648 &self,
1649 id: RemoteConnectionId,
1650 ) -> Result<RemoteConnectionOptions> {
1651 let (kind, host, port, user, distro, container_id, name, use_podman) =
1652 self.select_row_bound(sql!(
1653 SELECT kind, host, port, user, distro, container_id, name, use_podman
1654 FROM remote_connections
1655 WHERE id = ?
1656 ))?(id.0)?
1657 .context("no such remote connection")?;
1658 Self::remote_connection_from_row(
1659 kind,
1660 host,
1661 port,
1662 user,
1663 distro,
1664 container_id,
1665 name,
1666 use_podman,
1667 )
1668 .context("invalid remote_connection row")
1669 }
1670
1671 fn remote_connection_from_row(
1672 kind: String,
1673 host: Option<String>,
1674 port: Option<u16>,
1675 user: Option<String>,
1676 distro: Option<String>,
1677 container_id: Option<String>,
1678 name: Option<String>,
1679 use_podman: Option<bool>,
1680 ) -> Option<RemoteConnectionOptions> {
1681 match RemoteConnectionKind::deserialize(&kind)? {
1682 RemoteConnectionKind::Wsl => Some(RemoteConnectionOptions::Wsl(WslConnectionOptions {
1683 distro_name: distro?,
1684 user: user,
1685 })),
1686 RemoteConnectionKind::Ssh => Some(RemoteConnectionOptions::Ssh(SshConnectionOptions {
1687 host: host?.into(),
1688 port,
1689 username: user,
1690 ..Default::default()
1691 })),
1692 RemoteConnectionKind::Docker => {
1693 Some(RemoteConnectionOptions::Docker(DockerConnectionOptions {
1694 container_id: container_id?,
1695 name: name?,
1696 remote_user: user?,
1697 upload_binary_over_docker_exec: false,
1698 use_podman: use_podman?,
1699 }))
1700 }
1701 }
1702 }
1703
1704 query! {
1705 pub async fn delete_workspace_by_id(id: WorkspaceId) -> Result<()> {
1706 DELETE FROM workspaces
1707 WHERE workspace_id IS ?
1708 }
1709 }
1710
1711 // Returns the recent locations which are still valid on disk and deletes ones which no longer
1712 // exist.
1713 pub async fn recent_workspaces_on_disk(
1714 &self,
1715 ) -> Result<Vec<(WorkspaceId, SerializedWorkspaceLocation, PathList)>> {
1716 let mut result = Vec::new();
1717 let mut delete_tasks = Vec::new();
1718 let remote_connections = self.remote_connections()?;
1719
1720 for (id, paths, remote_connection_id) in self.recent_workspaces()? {
1721 if let Some(remote_connection_id) = remote_connection_id {
1722 if let Some(connection_options) = remote_connections.get(&remote_connection_id) {
1723 result.push((
1724 id,
1725 SerializedWorkspaceLocation::Remote(connection_options.clone()),
1726 paths,
1727 ));
1728 } else {
1729 delete_tasks.push(self.delete_workspace_by_id(id));
1730 }
1731 continue;
1732 }
1733
1734 let has_wsl_path = if cfg!(windows) {
1735 paths
1736 .paths()
1737 .iter()
1738 .any(|path| util::paths::WslPath::from_path(path).is_some())
1739 } else {
1740 false
1741 };
1742
1743 // Delete the workspace if any of the paths are WSL paths.
1744 // If a local workspace points to WSL, this check will cause us to wait for the
1745 // WSL VM and file server to boot up. This can block for many seconds.
1746 // Supported scenarios use remote workspaces.
1747 if !has_wsl_path && paths.paths().iter().all(|path| path.exists()) {
1748 // Only show directories in recent projects
1749 if paths.paths().iter().any(|path| path.is_dir()) {
1750 result.push((id, SerializedWorkspaceLocation::Local, paths));
1751 }
1752 } else {
1753 delete_tasks.push(self.delete_workspace_by_id(id));
1754 }
1755 }
1756
1757 futures::future::join_all(delete_tasks).await;
1758 Ok(result)
1759 }
1760
1761 pub async fn last_workspace(
1762 &self,
1763 ) -> Result<Option<(WorkspaceId, SerializedWorkspaceLocation, PathList)>> {
1764 Ok(self.recent_workspaces_on_disk().await?.into_iter().next())
1765 }
1766
1767 // Returns the locations of the workspaces that were still opened when the last
1768 // session was closed (i.e. when Zed was quit).
1769 // If `last_session_window_order` is provided, the returned locations are ordered
1770 // according to that.
1771 pub fn last_session_workspace_locations(
1772 &self,
1773 last_session_id: &str,
1774 last_session_window_stack: Option<Vec<WindowId>>,
1775 ) -> Result<Vec<(WorkspaceId, SerializedWorkspaceLocation, PathList)>> {
1776 let mut workspaces = Vec::new();
1777
1778 for (workspace_id, paths, window_id, remote_connection_id) in
1779 self.session_workspaces(last_session_id.to_owned())?
1780 {
1781 if let Some(remote_connection_id) = remote_connection_id {
1782 workspaces.push((
1783 workspace_id,
1784 SerializedWorkspaceLocation::Remote(
1785 self.remote_connection(remote_connection_id)?,
1786 ),
1787 paths,
1788 window_id.map(WindowId::from),
1789 ));
1790 } else if paths.is_empty() {
1791 // Empty workspace with items (drafts, files) - include for restoration
1792 workspaces.push((
1793 workspace_id,
1794 SerializedWorkspaceLocation::Local,
1795 paths,
1796 window_id.map(WindowId::from),
1797 ));
1798 } else if paths.paths().iter().all(|path| path.exists())
1799 && paths.paths().iter().any(|path| path.is_dir())
1800 {
1801 workspaces.push((
1802 workspace_id,
1803 SerializedWorkspaceLocation::Local,
1804 paths,
1805 window_id.map(WindowId::from),
1806 ));
1807 }
1808 }
1809
1810 if let Some(stack) = last_session_window_stack {
1811 workspaces.sort_by_key(|(_, _, _, window_id)| {
1812 window_id
1813 .and_then(|id| stack.iter().position(|&order_id| order_id == id))
1814 .unwrap_or(usize::MAX)
1815 });
1816 }
1817
1818 Ok(workspaces
1819 .into_iter()
1820 .map(|(workspace_id, location, paths, _)| (workspace_id, location, paths))
1821 .collect::<Vec<_>>())
1822 }
1823
1824 fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
1825 Ok(self
1826 .get_pane_group(workspace_id, None)?
1827 .into_iter()
1828 .next()
1829 .unwrap_or_else(|| {
1830 SerializedPaneGroup::Pane(SerializedPane {
1831 active: true,
1832 children: vec![],
1833 pinned_count: 0,
1834 })
1835 }))
1836 }
1837
1838 fn get_pane_group(
1839 &self,
1840 workspace_id: WorkspaceId,
1841 group_id: Option<GroupId>,
1842 ) -> Result<Vec<SerializedPaneGroup>> {
1843 type GroupKey = (Option<GroupId>, WorkspaceId);
1844 type GroupOrPane = (
1845 Option<GroupId>,
1846 Option<SerializedAxis>,
1847 Option<PaneId>,
1848 Option<bool>,
1849 Option<usize>,
1850 Option<String>,
1851 );
1852 self.select_bound::<GroupKey, GroupOrPane>(sql!(
1853 SELECT group_id, axis, pane_id, active, pinned_count, flexes
1854 FROM (SELECT
1855 group_id,
1856 axis,
1857 NULL as pane_id,
1858 NULL as active,
1859 NULL as pinned_count,
1860 position,
1861 parent_group_id,
1862 workspace_id,
1863 flexes
1864 FROM pane_groups
1865 UNION
1866 SELECT
1867 NULL,
1868 NULL,
1869 center_panes.pane_id,
1870 panes.active as active,
1871 pinned_count,
1872 position,
1873 parent_group_id,
1874 panes.workspace_id as workspace_id,
1875 NULL
1876 FROM center_panes
1877 JOIN panes ON center_panes.pane_id = panes.pane_id)
1878 WHERE parent_group_id IS ? AND workspace_id = ?
1879 ORDER BY position
1880 ))?((group_id, workspace_id))?
1881 .into_iter()
1882 .map(|(group_id, axis, pane_id, active, pinned_count, flexes)| {
1883 let maybe_pane = maybe!({ Some((pane_id?, active?, pinned_count?)) });
1884 if let Some((group_id, axis)) = group_id.zip(axis) {
1885 let flexes = flexes
1886 .map(|flexes: String| serde_json::from_str::<Vec<f32>>(&flexes))
1887 .transpose()?;
1888
1889 Ok(SerializedPaneGroup::Group {
1890 axis,
1891 children: self.get_pane_group(workspace_id, Some(group_id))?,
1892 flexes,
1893 })
1894 } else if let Some((pane_id, active, pinned_count)) = maybe_pane {
1895 Ok(SerializedPaneGroup::Pane(SerializedPane::new(
1896 self.get_items(pane_id)?,
1897 active,
1898 pinned_count,
1899 )))
1900 } else {
1901 bail!("Pane Group Child was neither a pane group or a pane");
1902 }
1903 })
1904 // Filter out panes and pane groups which don't have any children or items
1905 .filter(|pane_group| match pane_group {
1906 Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
1907 Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
1908 _ => true,
1909 })
1910 .collect::<Result<_>>()
1911 }
1912
1913 fn save_pane_group(
1914 conn: &Connection,
1915 workspace_id: WorkspaceId,
1916 pane_group: &SerializedPaneGroup,
1917 parent: Option<(GroupId, usize)>,
1918 ) -> Result<()> {
1919 if parent.is_none() {
1920 log::debug!("Saving a pane group for workspace {workspace_id:?}");
1921 }
1922 match pane_group {
1923 SerializedPaneGroup::Group {
1924 axis,
1925 children,
1926 flexes,
1927 } => {
1928 let (parent_id, position) = parent.unzip();
1929
1930 let flex_string = flexes
1931 .as_ref()
1932 .map(|flexes| serde_json::json!(flexes).to_string());
1933
1934 let group_id = conn.select_row_bound::<_, i64>(sql!(
1935 INSERT INTO pane_groups(
1936 workspace_id,
1937 parent_group_id,
1938 position,
1939 axis,
1940 flexes
1941 )
1942 VALUES (?, ?, ?, ?, ?)
1943 RETURNING group_id
1944 ))?((
1945 workspace_id,
1946 parent_id,
1947 position,
1948 *axis,
1949 flex_string,
1950 ))?
1951 .context("Couldn't retrieve group_id from inserted pane_group")?;
1952
1953 for (position, group) in children.iter().enumerate() {
1954 Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))?
1955 }
1956
1957 Ok(())
1958 }
1959 SerializedPaneGroup::Pane(pane) => {
1960 Self::save_pane(conn, workspace_id, pane, parent)?;
1961 Ok(())
1962 }
1963 }
1964 }
1965
1966 fn save_pane(
1967 conn: &Connection,
1968 workspace_id: WorkspaceId,
1969 pane: &SerializedPane,
1970 parent: Option<(GroupId, usize)>,
1971 ) -> Result<PaneId> {
1972 let pane_id = conn.select_row_bound::<_, i64>(sql!(
1973 INSERT INTO panes(workspace_id, active, pinned_count)
1974 VALUES (?, ?, ?)
1975 RETURNING pane_id
1976 ))?((workspace_id, pane.active, pane.pinned_count))?
1977 .context("Could not retrieve inserted pane_id")?;
1978
1979 let (parent_id, order) = parent.unzip();
1980 conn.exec_bound(sql!(
1981 INSERT INTO center_panes(pane_id, parent_group_id, position)
1982 VALUES (?, ?, ?)
1983 ))?((pane_id, parent_id, order))?;
1984
1985 Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
1986
1987 Ok(pane_id)
1988 }
1989
1990 fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
1991 self.select_bound(sql!(
1992 SELECT kind, item_id, active, preview FROM items
1993 WHERE pane_id = ?
1994 ORDER BY position
1995 ))?(pane_id)
1996 }
1997
1998 fn save_items(
1999 conn: &Connection,
2000 workspace_id: WorkspaceId,
2001 pane_id: PaneId,
2002 items: &[SerializedItem],
2003 ) -> Result<()> {
2004 let mut insert = conn.exec_bound(sql!(
2005 INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active, preview) VALUES (?, ?, ?, ?, ?, ?, ?)
2006 )).context("Preparing insertion")?;
2007 for (position, item) in items.iter().enumerate() {
2008 insert((workspace_id, pane_id, position, item))?;
2009 }
2010
2011 Ok(())
2012 }
2013
2014 query! {
2015 pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> {
2016 UPDATE workspaces
2017 SET timestamp = CURRENT_TIMESTAMP
2018 WHERE workspace_id = ?
2019 }
2020 }
2021
2022 query! {
2023 pub(crate) async fn set_window_open_status(workspace_id: WorkspaceId, bounds: SerializedWindowBounds, display: Uuid) -> Result<()> {
2024 UPDATE workspaces
2025 SET window_state = ?2,
2026 window_x = ?3,
2027 window_y = ?4,
2028 window_width = ?5,
2029 window_height = ?6,
2030 display = ?7
2031 WHERE workspace_id = ?1
2032 }
2033 }
2034
2035 query! {
2036 pub(crate) async fn set_centered_layout(workspace_id: WorkspaceId, centered_layout: bool) -> Result<()> {
2037 UPDATE workspaces
2038 SET centered_layout = ?2
2039 WHERE workspace_id = ?1
2040 }
2041 }
2042
2043 query! {
2044 pub(crate) async fn set_session_id(workspace_id: WorkspaceId, session_id: Option<String>) -> Result<()> {
2045 UPDATE workspaces
2046 SET session_id = ?2
2047 WHERE workspace_id = ?1
2048 }
2049 }
2050
2051 pub(crate) async fn toolchains(
2052 &self,
2053 workspace_id: WorkspaceId,
2054 ) -> Result<Vec<(Toolchain, Arc<Path>, Arc<RelPath>)>> {
2055 self.write(move |this| {
2056 let mut select = this
2057 .select_bound(sql!(
2058 SELECT
2059 name, path, worktree_root_path, relative_worktree_path, language_name, raw_json
2060 FROM toolchains
2061 WHERE workspace_id = ?
2062 ))
2063 .context("select toolchains")?;
2064
2065 let toolchain: Vec<(String, String, String, String, String, String)> =
2066 select(workspace_id)?;
2067
2068 Ok(toolchain
2069 .into_iter()
2070 .filter_map(
2071 |(name, path, worktree_root_path, relative_worktree_path, language, json)| {
2072 Some((
2073 Toolchain {
2074 name: name.into(),
2075 path: path.into(),
2076 language_name: LanguageName::new(&language),
2077 as_json: serde_json::Value::from_str(&json).ok()?,
2078 },
2079 Arc::from(worktree_root_path.as_ref()),
2080 RelPath::from_proto(&relative_worktree_path).log_err()?,
2081 ))
2082 },
2083 )
2084 .collect())
2085 })
2086 .await
2087 }
2088
2089 pub async fn set_toolchain(
2090 &self,
2091 workspace_id: WorkspaceId,
2092 worktree_root_path: Arc<Path>,
2093 relative_worktree_path: Arc<RelPath>,
2094 toolchain: Toolchain,
2095 ) -> Result<()> {
2096 log::debug!(
2097 "Setting toolchain for workspace, worktree: {worktree_root_path:?}, relative path: {relative_worktree_path:?}, toolchain: {}",
2098 toolchain.name
2099 );
2100 self.write(move |conn| {
2101 let mut insert = conn
2102 .exec_bound(sql!(
2103 INSERT INTO toolchains(workspace_id, worktree_root_path, relative_worktree_path, language_name, name, path, raw_json) VALUES (?, ?, ?, ?, ?, ?, ?)
2104 ON CONFLICT DO
2105 UPDATE SET
2106 name = ?5,
2107 path = ?6,
2108 raw_json = ?7
2109 ))
2110 .context("Preparing insertion")?;
2111
2112 insert((
2113 workspace_id,
2114 worktree_root_path.to_string_lossy().into_owned(),
2115 relative_worktree_path.as_unix_str(),
2116 toolchain.language_name.as_ref(),
2117 toolchain.name.as_ref(),
2118 toolchain.path.as_ref(),
2119 toolchain.as_json.to_string(),
2120 ))?;
2121
2122 Ok(())
2123 }).await
2124 }
2125
2126 pub(crate) async fn save_trusted_worktrees(
2127 &self,
2128 trusted_worktrees: HashMap<Option<RemoteHostLocation>, HashSet<PathBuf>>,
2129 ) -> anyhow::Result<()> {
2130 use anyhow::Context as _;
2131 use db::sqlez::statement::Statement;
2132 use itertools::Itertools as _;
2133
2134 DB.clear_trusted_worktrees()
2135 .await
2136 .context("clearing previous trust state")?;
2137
2138 let trusted_worktrees = trusted_worktrees
2139 .into_iter()
2140 .flat_map(|(host, abs_paths)| {
2141 abs_paths
2142 .into_iter()
2143 .map(move |abs_path| (Some(abs_path), host.clone()))
2144 })
2145 .collect::<Vec<_>>();
2146 let mut first_worktree;
2147 let mut last_worktree = 0_usize;
2148 for (count, placeholders) in std::iter::once("(?, ?, ?)")
2149 .cycle()
2150 .take(trusted_worktrees.len())
2151 .chunks(MAX_QUERY_PLACEHOLDERS / 3)
2152 .into_iter()
2153 .map(|chunk| {
2154 let mut count = 0;
2155 let placeholders = chunk
2156 .inspect(|_| {
2157 count += 1;
2158 })
2159 .join(", ");
2160 (count, placeholders)
2161 })
2162 .collect::<Vec<_>>()
2163 {
2164 first_worktree = last_worktree;
2165 last_worktree = last_worktree + count;
2166 let query = format!(
2167 r#"INSERT INTO trusted_worktrees(absolute_path, user_name, host_name)
2168VALUES {placeholders};"#
2169 );
2170
2171 let trusted_worktrees = trusted_worktrees[first_worktree..last_worktree].to_vec();
2172 self.write(move |conn| {
2173 let mut statement = Statement::prepare(conn, query)?;
2174 let mut next_index = 1;
2175 for (abs_path, host) in trusted_worktrees {
2176 let abs_path = abs_path.as_ref().map(|abs_path| abs_path.to_string_lossy());
2177 next_index = statement.bind(
2178 &abs_path.as_ref().map(|abs_path| abs_path.as_ref()),
2179 next_index,
2180 )?;
2181 next_index = statement.bind(
2182 &host
2183 .as_ref()
2184 .and_then(|host| Some(host.user_name.as_ref()?.as_str())),
2185 next_index,
2186 )?;
2187 next_index = statement.bind(
2188 &host.as_ref().map(|host| host.host_identifier.as_str()),
2189 next_index,
2190 )?;
2191 }
2192 statement.exec()
2193 })
2194 .await
2195 .context("inserting new trusted state")?;
2196 }
2197 Ok(())
2198 }
2199
2200 pub fn fetch_trusted_worktrees(&self) -> Result<DbTrustedPaths> {
2201 let trusted_worktrees = DB.trusted_worktrees()?;
2202 Ok(trusted_worktrees
2203 .into_iter()
2204 .filter_map(|(abs_path, user_name, host_name)| {
2205 let db_host = match (user_name, host_name) {
2206 (None, Some(host_name)) => Some(RemoteHostLocation {
2207 user_name: None,
2208 host_identifier: SharedString::new(host_name),
2209 }),
2210 (Some(user_name), Some(host_name)) => Some(RemoteHostLocation {
2211 user_name: Some(SharedString::new(user_name)),
2212 host_identifier: SharedString::new(host_name),
2213 }),
2214 _ => None,
2215 };
2216 Some((db_host, abs_path?))
2217 })
2218 .fold(HashMap::default(), |mut acc, (remote_host, abs_path)| {
2219 acc.entry(remote_host)
2220 .or_insert_with(HashSet::default)
2221 .insert(abs_path);
2222 acc
2223 }))
2224 }
2225
2226 query! {
2227 fn trusted_worktrees() -> Result<Vec<(Option<PathBuf>, Option<String>, Option<String>)>> {
2228 SELECT absolute_path, user_name, host_name
2229 FROM trusted_worktrees
2230 }
2231 }
2232
2233 query! {
2234 pub async fn clear_trusted_worktrees() -> Result<()> {
2235 DELETE FROM trusted_worktrees
2236 }
2237 }
2238}
2239
2240pub fn delete_unloaded_items(
2241 alive_items: Vec<ItemId>,
2242 workspace_id: WorkspaceId,
2243 table: &'static str,
2244 db: &ThreadSafeConnection,
2245 cx: &mut App,
2246) -> Task<Result<()>> {
2247 let db = db.clone();
2248 cx.spawn(async move |_| {
2249 let placeholders = alive_items
2250 .iter()
2251 .map(|_| "?")
2252 .collect::<Vec<&str>>()
2253 .join(", ");
2254
2255 let query = format!(
2256 "DELETE FROM {table} WHERE workspace_id = ? AND item_id NOT IN ({placeholders})"
2257 );
2258
2259 db.write(move |conn| {
2260 let mut statement = Statement::prepare(conn, query)?;
2261 let mut next_index = statement.bind(&workspace_id, 1)?;
2262 for id in alive_items {
2263 next_index = statement.bind(&id, next_index)?;
2264 }
2265 statement.exec()
2266 })
2267 .await
2268 })
2269}
2270
2271#[cfg(test)]
2272mod tests {
2273 use super::*;
2274 use crate::persistence::model::{
2275 SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
2276 };
2277 use gpui;
2278 use pretty_assertions::assert_eq;
2279 use remote::SshConnectionOptions;
2280 use std::{thread, time::Duration};
2281
2282 #[gpui::test]
2283 async fn test_breakpoints() {
2284 zlog::init_test();
2285
2286 let db = WorkspaceDb::open_test_db("test_breakpoints").await;
2287 let id = db.next_id().await.unwrap();
2288
2289 let path = Path::new("/tmp/test.rs");
2290
2291 let breakpoint = Breakpoint {
2292 position: 123,
2293 message: None,
2294 state: BreakpointState::Enabled,
2295 condition: None,
2296 hit_condition: None,
2297 };
2298
2299 let log_breakpoint = Breakpoint {
2300 position: 456,
2301 message: Some("Test log message".into()),
2302 state: BreakpointState::Enabled,
2303 condition: None,
2304 hit_condition: None,
2305 };
2306
2307 let disable_breakpoint = Breakpoint {
2308 position: 578,
2309 message: None,
2310 state: BreakpointState::Disabled,
2311 condition: None,
2312 hit_condition: None,
2313 };
2314
2315 let condition_breakpoint = Breakpoint {
2316 position: 789,
2317 message: None,
2318 state: BreakpointState::Enabled,
2319 condition: Some("x > 5".into()),
2320 hit_condition: None,
2321 };
2322
2323 let hit_condition_breakpoint = Breakpoint {
2324 position: 999,
2325 message: None,
2326 state: BreakpointState::Enabled,
2327 condition: None,
2328 hit_condition: Some(">= 3".into()),
2329 };
2330
2331 let workspace = SerializedWorkspace {
2332 id,
2333 paths: PathList::new(&["/tmp"]),
2334 location: SerializedWorkspaceLocation::Local,
2335 center_group: Default::default(),
2336 window_bounds: Default::default(),
2337 display: Default::default(),
2338 docks: Default::default(),
2339 centered_layout: false,
2340 breakpoints: {
2341 let mut map = collections::BTreeMap::default();
2342 map.insert(
2343 Arc::from(path),
2344 vec![
2345 SourceBreakpoint {
2346 row: breakpoint.position,
2347 path: Arc::from(path),
2348 message: breakpoint.message.clone(),
2349 state: breakpoint.state,
2350 condition: breakpoint.condition.clone(),
2351 hit_condition: breakpoint.hit_condition.clone(),
2352 },
2353 SourceBreakpoint {
2354 row: log_breakpoint.position,
2355 path: Arc::from(path),
2356 message: log_breakpoint.message.clone(),
2357 state: log_breakpoint.state,
2358 condition: log_breakpoint.condition.clone(),
2359 hit_condition: log_breakpoint.hit_condition.clone(),
2360 },
2361 SourceBreakpoint {
2362 row: disable_breakpoint.position,
2363 path: Arc::from(path),
2364 message: disable_breakpoint.message.clone(),
2365 state: disable_breakpoint.state,
2366 condition: disable_breakpoint.condition.clone(),
2367 hit_condition: disable_breakpoint.hit_condition.clone(),
2368 },
2369 SourceBreakpoint {
2370 row: condition_breakpoint.position,
2371 path: Arc::from(path),
2372 message: condition_breakpoint.message.clone(),
2373 state: condition_breakpoint.state,
2374 condition: condition_breakpoint.condition.clone(),
2375 hit_condition: condition_breakpoint.hit_condition.clone(),
2376 },
2377 SourceBreakpoint {
2378 row: hit_condition_breakpoint.position,
2379 path: Arc::from(path),
2380 message: hit_condition_breakpoint.message.clone(),
2381 state: hit_condition_breakpoint.state,
2382 condition: hit_condition_breakpoint.condition.clone(),
2383 hit_condition: hit_condition_breakpoint.hit_condition.clone(),
2384 },
2385 ],
2386 );
2387 map
2388 },
2389 session_id: None,
2390 window_id: None,
2391 user_toolchains: Default::default(),
2392 };
2393
2394 db.save_workspace(workspace.clone()).await;
2395
2396 let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
2397 let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(path)).unwrap();
2398
2399 assert_eq!(loaded_breakpoints.len(), 5);
2400
2401 // normal breakpoint
2402 assert_eq!(loaded_breakpoints[0].row, breakpoint.position);
2403 assert_eq!(loaded_breakpoints[0].message, breakpoint.message);
2404 assert_eq!(loaded_breakpoints[0].condition, breakpoint.condition);
2405 assert_eq!(
2406 loaded_breakpoints[0].hit_condition,
2407 breakpoint.hit_condition
2408 );
2409 assert_eq!(loaded_breakpoints[0].state, breakpoint.state);
2410 assert_eq!(loaded_breakpoints[0].path, Arc::from(path));
2411
2412 // enabled breakpoint
2413 assert_eq!(loaded_breakpoints[1].row, log_breakpoint.position);
2414 assert_eq!(loaded_breakpoints[1].message, log_breakpoint.message);
2415 assert_eq!(loaded_breakpoints[1].condition, log_breakpoint.condition);
2416 assert_eq!(
2417 loaded_breakpoints[1].hit_condition,
2418 log_breakpoint.hit_condition
2419 );
2420 assert_eq!(loaded_breakpoints[1].state, log_breakpoint.state);
2421 assert_eq!(loaded_breakpoints[1].path, Arc::from(path));
2422
2423 // disable breakpoint
2424 assert_eq!(loaded_breakpoints[2].row, disable_breakpoint.position);
2425 assert_eq!(loaded_breakpoints[2].message, disable_breakpoint.message);
2426 assert_eq!(
2427 loaded_breakpoints[2].condition,
2428 disable_breakpoint.condition
2429 );
2430 assert_eq!(
2431 loaded_breakpoints[2].hit_condition,
2432 disable_breakpoint.hit_condition
2433 );
2434 assert_eq!(loaded_breakpoints[2].state, disable_breakpoint.state);
2435 assert_eq!(loaded_breakpoints[2].path, Arc::from(path));
2436
2437 // condition breakpoint
2438 assert_eq!(loaded_breakpoints[3].row, condition_breakpoint.position);
2439 assert_eq!(loaded_breakpoints[3].message, condition_breakpoint.message);
2440 assert_eq!(
2441 loaded_breakpoints[3].condition,
2442 condition_breakpoint.condition
2443 );
2444 assert_eq!(
2445 loaded_breakpoints[3].hit_condition,
2446 condition_breakpoint.hit_condition
2447 );
2448 assert_eq!(loaded_breakpoints[3].state, condition_breakpoint.state);
2449 assert_eq!(loaded_breakpoints[3].path, Arc::from(path));
2450
2451 // hit condition breakpoint
2452 assert_eq!(loaded_breakpoints[4].row, hit_condition_breakpoint.position);
2453 assert_eq!(
2454 loaded_breakpoints[4].message,
2455 hit_condition_breakpoint.message
2456 );
2457 assert_eq!(
2458 loaded_breakpoints[4].condition,
2459 hit_condition_breakpoint.condition
2460 );
2461 assert_eq!(
2462 loaded_breakpoints[4].hit_condition,
2463 hit_condition_breakpoint.hit_condition
2464 );
2465 assert_eq!(loaded_breakpoints[4].state, hit_condition_breakpoint.state);
2466 assert_eq!(loaded_breakpoints[4].path, Arc::from(path));
2467 }
2468
2469 #[gpui::test]
2470 async fn test_remove_last_breakpoint() {
2471 zlog::init_test();
2472
2473 let db = WorkspaceDb::open_test_db("test_remove_last_breakpoint").await;
2474 let id = db.next_id().await.unwrap();
2475
2476 let singular_path = Path::new("/tmp/test_remove_last_breakpoint.rs");
2477
2478 let breakpoint_to_remove = Breakpoint {
2479 position: 100,
2480 message: None,
2481 state: BreakpointState::Enabled,
2482 condition: None,
2483 hit_condition: None,
2484 };
2485
2486 let workspace = SerializedWorkspace {
2487 id,
2488 paths: PathList::new(&["/tmp"]),
2489 location: SerializedWorkspaceLocation::Local,
2490 center_group: Default::default(),
2491 window_bounds: Default::default(),
2492 display: Default::default(),
2493 docks: Default::default(),
2494 centered_layout: false,
2495 breakpoints: {
2496 let mut map = collections::BTreeMap::default();
2497 map.insert(
2498 Arc::from(singular_path),
2499 vec![SourceBreakpoint {
2500 row: breakpoint_to_remove.position,
2501 path: Arc::from(singular_path),
2502 message: None,
2503 state: BreakpointState::Enabled,
2504 condition: None,
2505 hit_condition: None,
2506 }],
2507 );
2508 map
2509 },
2510 session_id: None,
2511 window_id: None,
2512 user_toolchains: Default::default(),
2513 };
2514
2515 db.save_workspace(workspace.clone()).await;
2516
2517 let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
2518 let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(singular_path)).unwrap();
2519
2520 assert_eq!(loaded_breakpoints.len(), 1);
2521 assert_eq!(loaded_breakpoints[0].row, breakpoint_to_remove.position);
2522 assert_eq!(loaded_breakpoints[0].message, breakpoint_to_remove.message);
2523 assert_eq!(
2524 loaded_breakpoints[0].condition,
2525 breakpoint_to_remove.condition
2526 );
2527 assert_eq!(
2528 loaded_breakpoints[0].hit_condition,
2529 breakpoint_to_remove.hit_condition
2530 );
2531 assert_eq!(loaded_breakpoints[0].state, breakpoint_to_remove.state);
2532 assert_eq!(loaded_breakpoints[0].path, Arc::from(singular_path));
2533
2534 let workspace_without_breakpoint = SerializedWorkspace {
2535 id,
2536 paths: PathList::new(&["/tmp"]),
2537 location: SerializedWorkspaceLocation::Local,
2538 center_group: Default::default(),
2539 window_bounds: Default::default(),
2540 display: Default::default(),
2541 docks: Default::default(),
2542 centered_layout: false,
2543 breakpoints: collections::BTreeMap::default(),
2544 session_id: None,
2545 window_id: None,
2546 user_toolchains: Default::default(),
2547 };
2548
2549 db.save_workspace(workspace_without_breakpoint.clone())
2550 .await;
2551
2552 let loaded_after_remove = db.workspace_for_roots(&["/tmp"]).unwrap();
2553 let empty_breakpoints = loaded_after_remove
2554 .breakpoints
2555 .get(&Arc::from(singular_path));
2556
2557 assert!(empty_breakpoints.is_none());
2558 }
2559
2560 #[gpui::test]
2561 async fn test_next_id_stability() {
2562 zlog::init_test();
2563
2564 let db = WorkspaceDb::open_test_db("test_next_id_stability").await;
2565
2566 db.write(|conn| {
2567 conn.migrate(
2568 "test_table",
2569 &[sql!(
2570 CREATE TABLE test_table(
2571 text TEXT,
2572 workspace_id INTEGER,
2573 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
2574 ON DELETE CASCADE
2575 ) STRICT;
2576 )],
2577 |_, _, _| false,
2578 )
2579 .unwrap();
2580 })
2581 .await;
2582
2583 let id = db.next_id().await.unwrap();
2584 // Assert the empty row got inserted
2585 assert_eq!(
2586 Some(id),
2587 db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
2588 SELECT workspace_id FROM workspaces WHERE workspace_id = ?
2589 ))
2590 .unwrap()(id)
2591 .unwrap()
2592 );
2593
2594 db.write(move |conn| {
2595 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2596 .unwrap()(("test-text-1", id))
2597 .unwrap()
2598 })
2599 .await;
2600
2601 let test_text_1 = db
2602 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2603 .unwrap()(1)
2604 .unwrap()
2605 .unwrap();
2606 assert_eq!(test_text_1, "test-text-1");
2607 }
2608
2609 #[gpui::test]
2610 async fn test_workspace_id_stability() {
2611 zlog::init_test();
2612
2613 let db = WorkspaceDb::open_test_db("test_workspace_id_stability").await;
2614
2615 db.write(|conn| {
2616 conn.migrate(
2617 "test_table",
2618 &[sql!(
2619 CREATE TABLE test_table(
2620 text TEXT,
2621 workspace_id INTEGER,
2622 FOREIGN KEY(workspace_id)
2623 REFERENCES workspaces(workspace_id)
2624 ON DELETE CASCADE
2625 ) STRICT;)],
2626 |_, _, _| false,
2627 )
2628 })
2629 .await
2630 .unwrap();
2631
2632 let mut workspace_1 = SerializedWorkspace {
2633 id: WorkspaceId(1),
2634 paths: PathList::new(&["/tmp", "/tmp2"]),
2635 location: SerializedWorkspaceLocation::Local,
2636 center_group: Default::default(),
2637 window_bounds: Default::default(),
2638 display: Default::default(),
2639 docks: Default::default(),
2640 centered_layout: false,
2641 breakpoints: Default::default(),
2642 session_id: None,
2643 window_id: None,
2644 user_toolchains: Default::default(),
2645 };
2646
2647 let workspace_2 = SerializedWorkspace {
2648 id: WorkspaceId(2),
2649 paths: PathList::new(&["/tmp"]),
2650 location: SerializedWorkspaceLocation::Local,
2651 center_group: Default::default(),
2652 window_bounds: Default::default(),
2653 display: Default::default(),
2654 docks: Default::default(),
2655 centered_layout: false,
2656 breakpoints: Default::default(),
2657 session_id: None,
2658 window_id: None,
2659 user_toolchains: Default::default(),
2660 };
2661
2662 db.save_workspace(workspace_1.clone()).await;
2663
2664 db.write(|conn| {
2665 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2666 .unwrap()(("test-text-1", 1))
2667 .unwrap();
2668 })
2669 .await;
2670
2671 db.save_workspace(workspace_2.clone()).await;
2672
2673 db.write(|conn| {
2674 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2675 .unwrap()(("test-text-2", 2))
2676 .unwrap();
2677 })
2678 .await;
2679
2680 workspace_1.paths = PathList::new(&["/tmp", "/tmp3"]);
2681 db.save_workspace(workspace_1.clone()).await;
2682 db.save_workspace(workspace_1).await;
2683 db.save_workspace(workspace_2).await;
2684
2685 let test_text_2 = db
2686 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2687 .unwrap()(2)
2688 .unwrap()
2689 .unwrap();
2690 assert_eq!(test_text_2, "test-text-2");
2691
2692 let test_text_1 = db
2693 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2694 .unwrap()(1)
2695 .unwrap()
2696 .unwrap();
2697 assert_eq!(test_text_1, "test-text-1");
2698 }
2699
2700 fn group(axis: Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
2701 SerializedPaneGroup::Group {
2702 axis: SerializedAxis(axis),
2703 flexes: None,
2704 children,
2705 }
2706 }
2707
2708 #[gpui::test]
2709 async fn test_full_workspace_serialization() {
2710 zlog::init_test();
2711
2712 let db = WorkspaceDb::open_test_db("test_full_workspace_serialization").await;
2713
2714 // -----------------
2715 // | 1,2 | 5,6 |
2716 // | - - - | |
2717 // | 3,4 | |
2718 // -----------------
2719 let center_group = group(
2720 Axis::Horizontal,
2721 vec![
2722 group(
2723 Axis::Vertical,
2724 vec![
2725 SerializedPaneGroup::Pane(SerializedPane::new(
2726 vec![
2727 SerializedItem::new("Terminal", 5, false, false),
2728 SerializedItem::new("Terminal", 6, true, false),
2729 ],
2730 false,
2731 0,
2732 )),
2733 SerializedPaneGroup::Pane(SerializedPane::new(
2734 vec![
2735 SerializedItem::new("Terminal", 7, true, false),
2736 SerializedItem::new("Terminal", 8, false, false),
2737 ],
2738 false,
2739 0,
2740 )),
2741 ],
2742 ),
2743 SerializedPaneGroup::Pane(SerializedPane::new(
2744 vec![
2745 SerializedItem::new("Terminal", 9, false, false),
2746 SerializedItem::new("Terminal", 10, true, false),
2747 ],
2748 false,
2749 0,
2750 )),
2751 ],
2752 );
2753
2754 let workspace = SerializedWorkspace {
2755 id: WorkspaceId(5),
2756 paths: PathList::new(&["/tmp", "/tmp2"]),
2757 location: SerializedWorkspaceLocation::Local,
2758 center_group,
2759 window_bounds: Default::default(),
2760 breakpoints: Default::default(),
2761 display: Default::default(),
2762 docks: Default::default(),
2763 centered_layout: false,
2764 session_id: None,
2765 window_id: Some(999),
2766 user_toolchains: Default::default(),
2767 };
2768
2769 db.save_workspace(workspace.clone()).await;
2770
2771 let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
2772 assert_eq!(workspace, round_trip_workspace.unwrap());
2773
2774 // Test guaranteed duplicate IDs
2775 db.save_workspace(workspace.clone()).await;
2776 db.save_workspace(workspace.clone()).await;
2777
2778 let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
2779 assert_eq!(workspace, round_trip_workspace.unwrap());
2780 }
2781
2782 #[gpui::test]
2783 async fn test_workspace_assignment() {
2784 zlog::init_test();
2785
2786 let db = WorkspaceDb::open_test_db("test_basic_functionality").await;
2787
2788 let workspace_1 = SerializedWorkspace {
2789 id: WorkspaceId(1),
2790 paths: PathList::new(&["/tmp", "/tmp2"]),
2791 location: SerializedWorkspaceLocation::Local,
2792 center_group: Default::default(),
2793 window_bounds: Default::default(),
2794 breakpoints: Default::default(),
2795 display: Default::default(),
2796 docks: Default::default(),
2797 centered_layout: false,
2798 session_id: None,
2799 window_id: Some(1),
2800 user_toolchains: Default::default(),
2801 };
2802
2803 let mut workspace_2 = SerializedWorkspace {
2804 id: WorkspaceId(2),
2805 paths: PathList::new(&["/tmp"]),
2806 location: SerializedWorkspaceLocation::Local,
2807 center_group: Default::default(),
2808 window_bounds: Default::default(),
2809 display: Default::default(),
2810 docks: Default::default(),
2811 centered_layout: false,
2812 breakpoints: Default::default(),
2813 session_id: None,
2814 window_id: Some(2),
2815 user_toolchains: Default::default(),
2816 };
2817
2818 db.save_workspace(workspace_1.clone()).await;
2819 db.save_workspace(workspace_2.clone()).await;
2820
2821 // Test that paths are treated as a set
2822 assert_eq!(
2823 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2824 workspace_1
2825 );
2826 assert_eq!(
2827 db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
2828 workspace_1
2829 );
2830
2831 // Make sure that other keys work
2832 assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
2833 assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
2834
2835 // Test 'mutate' case of updating a pre-existing id
2836 workspace_2.paths = PathList::new(&["/tmp", "/tmp2"]);
2837
2838 db.save_workspace(workspace_2.clone()).await;
2839 assert_eq!(
2840 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2841 workspace_2
2842 );
2843
2844 // Test other mechanism for mutating
2845 let mut workspace_3 = SerializedWorkspace {
2846 id: WorkspaceId(3),
2847 paths: PathList::new(&["/tmp2", "/tmp"]),
2848 location: SerializedWorkspaceLocation::Local,
2849 center_group: Default::default(),
2850 window_bounds: Default::default(),
2851 breakpoints: Default::default(),
2852 display: Default::default(),
2853 docks: Default::default(),
2854 centered_layout: false,
2855 session_id: None,
2856 window_id: Some(3),
2857 user_toolchains: Default::default(),
2858 };
2859
2860 db.save_workspace(workspace_3.clone()).await;
2861 assert_eq!(
2862 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2863 workspace_3
2864 );
2865
2866 // Make sure that updating paths differently also works
2867 workspace_3.paths = PathList::new(&["/tmp3", "/tmp4", "/tmp2"]);
2868 db.save_workspace(workspace_3.clone()).await;
2869 assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
2870 assert_eq!(
2871 db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
2872 .unwrap(),
2873 workspace_3
2874 );
2875 }
2876
2877 #[gpui::test]
2878 async fn test_session_workspaces() {
2879 zlog::init_test();
2880
2881 let db = WorkspaceDb::open_test_db("test_serializing_workspaces_session_id").await;
2882
2883 let workspace_1 = SerializedWorkspace {
2884 id: WorkspaceId(1),
2885 paths: PathList::new(&["/tmp1"]),
2886 location: SerializedWorkspaceLocation::Local,
2887 center_group: Default::default(),
2888 window_bounds: Default::default(),
2889 display: Default::default(),
2890 docks: Default::default(),
2891 centered_layout: false,
2892 breakpoints: Default::default(),
2893 session_id: Some("session-id-1".to_owned()),
2894 window_id: Some(10),
2895 user_toolchains: Default::default(),
2896 };
2897
2898 let workspace_2 = SerializedWorkspace {
2899 id: WorkspaceId(2),
2900 paths: PathList::new(&["/tmp2"]),
2901 location: SerializedWorkspaceLocation::Local,
2902 center_group: Default::default(),
2903 window_bounds: Default::default(),
2904 display: Default::default(),
2905 docks: Default::default(),
2906 centered_layout: false,
2907 breakpoints: Default::default(),
2908 session_id: Some("session-id-1".to_owned()),
2909 window_id: Some(20),
2910 user_toolchains: Default::default(),
2911 };
2912
2913 let workspace_3 = SerializedWorkspace {
2914 id: WorkspaceId(3),
2915 paths: PathList::new(&["/tmp3"]),
2916 location: SerializedWorkspaceLocation::Local,
2917 center_group: Default::default(),
2918 window_bounds: Default::default(),
2919 display: Default::default(),
2920 docks: Default::default(),
2921 centered_layout: false,
2922 breakpoints: Default::default(),
2923 session_id: Some("session-id-2".to_owned()),
2924 window_id: Some(30),
2925 user_toolchains: Default::default(),
2926 };
2927
2928 let workspace_4 = SerializedWorkspace {
2929 id: WorkspaceId(4),
2930 paths: PathList::new(&["/tmp4"]),
2931 location: SerializedWorkspaceLocation::Local,
2932 center_group: Default::default(),
2933 window_bounds: Default::default(),
2934 display: Default::default(),
2935 docks: Default::default(),
2936 centered_layout: false,
2937 breakpoints: Default::default(),
2938 session_id: None,
2939 window_id: None,
2940 user_toolchains: Default::default(),
2941 };
2942
2943 let connection_id = db
2944 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2945 host: "my-host".into(),
2946 port: Some(1234),
2947 ..Default::default()
2948 }))
2949 .await
2950 .unwrap();
2951
2952 let workspace_5 = SerializedWorkspace {
2953 id: WorkspaceId(5),
2954 paths: PathList::default(),
2955 location: SerializedWorkspaceLocation::Remote(
2956 db.remote_connection(connection_id).unwrap(),
2957 ),
2958 center_group: Default::default(),
2959 window_bounds: Default::default(),
2960 display: Default::default(),
2961 docks: Default::default(),
2962 centered_layout: false,
2963 breakpoints: Default::default(),
2964 session_id: Some("session-id-2".to_owned()),
2965 window_id: Some(50),
2966 user_toolchains: Default::default(),
2967 };
2968
2969 let workspace_6 = SerializedWorkspace {
2970 id: WorkspaceId(6),
2971 paths: PathList::new(&["/tmp6c", "/tmp6b", "/tmp6a"]),
2972 location: SerializedWorkspaceLocation::Local,
2973 center_group: Default::default(),
2974 window_bounds: Default::default(),
2975 breakpoints: Default::default(),
2976 display: Default::default(),
2977 docks: Default::default(),
2978 centered_layout: false,
2979 session_id: Some("session-id-3".to_owned()),
2980 window_id: Some(60),
2981 user_toolchains: Default::default(),
2982 };
2983
2984 db.save_workspace(workspace_1.clone()).await;
2985 thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
2986 db.save_workspace(workspace_2.clone()).await;
2987 db.save_workspace(workspace_3.clone()).await;
2988 thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
2989 db.save_workspace(workspace_4.clone()).await;
2990 db.save_workspace(workspace_5.clone()).await;
2991 db.save_workspace(workspace_6.clone()).await;
2992
2993 let locations = db.session_workspaces("session-id-1".to_owned()).unwrap();
2994 assert_eq!(locations.len(), 2);
2995 assert_eq!(locations[0].0, WorkspaceId(2));
2996 assert_eq!(locations[0].1, PathList::new(&["/tmp2"]));
2997 assert_eq!(locations[0].2, Some(20));
2998 assert_eq!(locations[1].0, WorkspaceId(1));
2999 assert_eq!(locations[1].1, PathList::new(&["/tmp1"]));
3000 assert_eq!(locations[1].2, Some(10));
3001
3002 let locations = db.session_workspaces("session-id-2".to_owned()).unwrap();
3003 assert_eq!(locations.len(), 2);
3004 assert_eq!(locations[0].0, WorkspaceId(5));
3005 assert_eq!(locations[0].1, PathList::default());
3006 assert_eq!(locations[0].2, Some(50));
3007 assert_eq!(locations[0].3, Some(connection_id));
3008 assert_eq!(locations[1].0, WorkspaceId(3));
3009 assert_eq!(locations[1].1, PathList::new(&["/tmp3"]));
3010 assert_eq!(locations[1].2, Some(30));
3011
3012 let locations = db.session_workspaces("session-id-3".to_owned()).unwrap();
3013 assert_eq!(locations.len(), 1);
3014 assert_eq!(locations[0].0, WorkspaceId(6));
3015 assert_eq!(
3016 locations[0].1,
3017 PathList::new(&["/tmp6c", "/tmp6b", "/tmp6a"]),
3018 );
3019 assert_eq!(locations[0].2, Some(60));
3020 }
3021
3022 fn default_workspace<P: AsRef<Path>>(
3023 paths: &[P],
3024 center_group: &SerializedPaneGroup,
3025 ) -> SerializedWorkspace {
3026 SerializedWorkspace {
3027 id: WorkspaceId(4),
3028 paths: PathList::new(paths),
3029 location: SerializedWorkspaceLocation::Local,
3030 center_group: center_group.clone(),
3031 window_bounds: Default::default(),
3032 display: Default::default(),
3033 docks: Default::default(),
3034 breakpoints: Default::default(),
3035 centered_layout: false,
3036 session_id: None,
3037 window_id: None,
3038 user_toolchains: Default::default(),
3039 }
3040 }
3041
3042 #[gpui::test]
3043 async fn test_last_session_workspace_locations() {
3044 let dir1 = tempfile::TempDir::with_prefix("dir1").unwrap();
3045 let dir2 = tempfile::TempDir::with_prefix("dir2").unwrap();
3046 let dir3 = tempfile::TempDir::with_prefix("dir3").unwrap();
3047 let dir4 = tempfile::TempDir::with_prefix("dir4").unwrap();
3048
3049 let db =
3050 WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces").await;
3051
3052 let workspaces = [
3053 (1, vec![dir1.path()], 9),
3054 (2, vec![dir2.path()], 5),
3055 (3, vec![dir3.path()], 8),
3056 (4, vec![dir4.path()], 2),
3057 (5, vec![dir1.path(), dir2.path(), dir3.path()], 3),
3058 (6, vec![dir4.path(), dir3.path(), dir2.path()], 4),
3059 ]
3060 .into_iter()
3061 .map(|(id, paths, window_id)| SerializedWorkspace {
3062 id: WorkspaceId(id),
3063 paths: PathList::new(paths.as_slice()),
3064 location: SerializedWorkspaceLocation::Local,
3065 center_group: Default::default(),
3066 window_bounds: Default::default(),
3067 display: Default::default(),
3068 docks: Default::default(),
3069 centered_layout: false,
3070 session_id: Some("one-session".to_owned()),
3071 breakpoints: Default::default(),
3072 window_id: Some(window_id),
3073 user_toolchains: Default::default(),
3074 })
3075 .collect::<Vec<_>>();
3076
3077 for workspace in workspaces.iter() {
3078 db.save_workspace(workspace.clone()).await;
3079 }
3080
3081 let stack = Some(Vec::from([
3082 WindowId::from(2), // Top
3083 WindowId::from(8),
3084 WindowId::from(5),
3085 WindowId::from(9),
3086 WindowId::from(3),
3087 WindowId::from(4), // Bottom
3088 ]));
3089
3090 let locations = db
3091 .last_session_workspace_locations("one-session", stack)
3092 .unwrap();
3093 assert_eq!(
3094 locations,
3095 [
3096 (
3097 WorkspaceId(4),
3098 SerializedWorkspaceLocation::Local,
3099 PathList::new(&[dir4.path()])
3100 ),
3101 (
3102 WorkspaceId(3),
3103 SerializedWorkspaceLocation::Local,
3104 PathList::new(&[dir3.path()])
3105 ),
3106 (
3107 WorkspaceId(2),
3108 SerializedWorkspaceLocation::Local,
3109 PathList::new(&[dir2.path()])
3110 ),
3111 (
3112 WorkspaceId(1),
3113 SerializedWorkspaceLocation::Local,
3114 PathList::new(&[dir1.path()])
3115 ),
3116 (
3117 WorkspaceId(5),
3118 SerializedWorkspaceLocation::Local,
3119 PathList::new(&[dir1.path(), dir2.path(), dir3.path()])
3120 ),
3121 (
3122 WorkspaceId(6),
3123 SerializedWorkspaceLocation::Local,
3124 PathList::new(&[dir4.path(), dir3.path(), dir2.path()])
3125 ),
3126 ]
3127 );
3128 }
3129
3130 #[gpui::test]
3131 async fn test_last_session_workspace_locations_remote() {
3132 let db =
3133 WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces_remote")
3134 .await;
3135
3136 let remote_connections = [
3137 ("host-1", "my-user-1"),
3138 ("host-2", "my-user-2"),
3139 ("host-3", "my-user-3"),
3140 ("host-4", "my-user-4"),
3141 ]
3142 .into_iter()
3143 .map(|(host, user)| async {
3144 let options = RemoteConnectionOptions::Ssh(SshConnectionOptions {
3145 host: host.into(),
3146 username: Some(user.to_string()),
3147 ..Default::default()
3148 });
3149 db.get_or_create_remote_connection(options.clone())
3150 .await
3151 .unwrap();
3152 options
3153 })
3154 .collect::<Vec<_>>();
3155
3156 let remote_connections = futures::future::join_all(remote_connections).await;
3157
3158 let workspaces = [
3159 (1, remote_connections[0].clone(), 9),
3160 (2, remote_connections[1].clone(), 5),
3161 (3, remote_connections[2].clone(), 8),
3162 (4, remote_connections[3].clone(), 2),
3163 ]
3164 .into_iter()
3165 .map(|(id, remote_connection, window_id)| SerializedWorkspace {
3166 id: WorkspaceId(id),
3167 paths: PathList::default(),
3168 location: SerializedWorkspaceLocation::Remote(remote_connection),
3169 center_group: Default::default(),
3170 window_bounds: Default::default(),
3171 display: Default::default(),
3172 docks: Default::default(),
3173 centered_layout: false,
3174 session_id: Some("one-session".to_owned()),
3175 breakpoints: Default::default(),
3176 window_id: Some(window_id),
3177 user_toolchains: Default::default(),
3178 })
3179 .collect::<Vec<_>>();
3180
3181 for workspace in workspaces.iter() {
3182 db.save_workspace(workspace.clone()).await;
3183 }
3184
3185 let stack = Some(Vec::from([
3186 WindowId::from(2), // Top
3187 WindowId::from(8),
3188 WindowId::from(5),
3189 WindowId::from(9), // Bottom
3190 ]));
3191
3192 let have = db
3193 .last_session_workspace_locations("one-session", stack)
3194 .unwrap();
3195 assert_eq!(have.len(), 4);
3196 assert_eq!(
3197 have[0],
3198 (
3199 WorkspaceId(4),
3200 SerializedWorkspaceLocation::Remote(remote_connections[3].clone()),
3201 PathList::default()
3202 )
3203 );
3204 assert_eq!(
3205 have[1],
3206 (
3207 WorkspaceId(3),
3208 SerializedWorkspaceLocation::Remote(remote_connections[2].clone()),
3209 PathList::default()
3210 )
3211 );
3212 assert_eq!(
3213 have[2],
3214 (
3215 WorkspaceId(2),
3216 SerializedWorkspaceLocation::Remote(remote_connections[1].clone()),
3217 PathList::default()
3218 )
3219 );
3220 assert_eq!(
3221 have[3],
3222 (
3223 WorkspaceId(1),
3224 SerializedWorkspaceLocation::Remote(remote_connections[0].clone()),
3225 PathList::default()
3226 )
3227 );
3228 }
3229
3230 #[gpui::test]
3231 async fn test_get_or_create_ssh_project() {
3232 let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project").await;
3233
3234 let host = "example.com".to_string();
3235 let port = Some(22_u16);
3236 let user = Some("user".to_string());
3237
3238 let connection_id = db
3239 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3240 host: host.clone().into(),
3241 port,
3242 username: user.clone(),
3243 ..Default::default()
3244 }))
3245 .await
3246 .unwrap();
3247
3248 // Test that calling the function again with the same parameters returns the same project
3249 let same_connection = db
3250 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3251 host: host.clone().into(),
3252 port,
3253 username: user.clone(),
3254 ..Default::default()
3255 }))
3256 .await
3257 .unwrap();
3258
3259 assert_eq!(connection_id, same_connection);
3260
3261 // Test with different parameters
3262 let host2 = "otherexample.com".to_string();
3263 let port2 = None;
3264 let user2 = Some("otheruser".to_string());
3265
3266 let different_connection = db
3267 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3268 host: host2.clone().into(),
3269 port: port2,
3270 username: user2.clone(),
3271 ..Default::default()
3272 }))
3273 .await
3274 .unwrap();
3275
3276 assert_ne!(connection_id, different_connection);
3277 }
3278
3279 #[gpui::test]
3280 async fn test_get_or_create_ssh_project_with_null_user() {
3281 let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project_with_null_user").await;
3282
3283 let (host, port, user) = ("example.com".to_string(), None, None);
3284
3285 let connection_id = db
3286 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3287 host: host.clone().into(),
3288 port,
3289 username: None,
3290 ..Default::default()
3291 }))
3292 .await
3293 .unwrap();
3294
3295 let same_connection_id = db
3296 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3297 host: host.clone().into(),
3298 port,
3299 username: user.clone(),
3300 ..Default::default()
3301 }))
3302 .await
3303 .unwrap();
3304
3305 assert_eq!(connection_id, same_connection_id);
3306 }
3307
3308 #[gpui::test]
3309 async fn test_get_remote_connections() {
3310 let db = WorkspaceDb::open_test_db("test_get_remote_connections").await;
3311
3312 let connections = [
3313 ("example.com".to_string(), None, None),
3314 (
3315 "anotherexample.com".to_string(),
3316 Some(123_u16),
3317 Some("user2".to_string()),
3318 ),
3319 ("yetanother.com".to_string(), Some(345_u16), None),
3320 ];
3321
3322 let mut ids = Vec::new();
3323 for (host, port, user) in connections.iter() {
3324 ids.push(
3325 db.get_or_create_remote_connection(RemoteConnectionOptions::Ssh(
3326 SshConnectionOptions {
3327 host: host.clone().into(),
3328 port: *port,
3329 username: user.clone(),
3330 ..Default::default()
3331 },
3332 ))
3333 .await
3334 .unwrap(),
3335 );
3336 }
3337
3338 let stored_connections = db.remote_connections().unwrap();
3339 assert_eq!(
3340 stored_connections,
3341 [
3342 (
3343 ids[0],
3344 RemoteConnectionOptions::Ssh(SshConnectionOptions {
3345 host: "example.com".into(),
3346 port: None,
3347 username: None,
3348 ..Default::default()
3349 }),
3350 ),
3351 (
3352 ids[1],
3353 RemoteConnectionOptions::Ssh(SshConnectionOptions {
3354 host: "anotherexample.com".into(),
3355 port: Some(123),
3356 username: Some("user2".into()),
3357 ..Default::default()
3358 }),
3359 ),
3360 (
3361 ids[2],
3362 RemoteConnectionOptions::Ssh(SshConnectionOptions {
3363 host: "yetanother.com".into(),
3364 port: Some(345),
3365 username: None,
3366 ..Default::default()
3367 }),
3368 ),
3369 ]
3370 .into_iter()
3371 .collect::<HashMap<_, _>>(),
3372 );
3373 }
3374
3375 #[gpui::test]
3376 async fn test_simple_split() {
3377 zlog::init_test();
3378
3379 let db = WorkspaceDb::open_test_db("simple_split").await;
3380
3381 // -----------------
3382 // | 1,2 | 5,6 |
3383 // | - - - | |
3384 // | 3,4 | |
3385 // -----------------
3386 let center_pane = group(
3387 Axis::Horizontal,
3388 vec![
3389 group(
3390 Axis::Vertical,
3391 vec![
3392 SerializedPaneGroup::Pane(SerializedPane::new(
3393 vec![
3394 SerializedItem::new("Terminal", 1, false, false),
3395 SerializedItem::new("Terminal", 2, true, false),
3396 ],
3397 false,
3398 0,
3399 )),
3400 SerializedPaneGroup::Pane(SerializedPane::new(
3401 vec![
3402 SerializedItem::new("Terminal", 4, false, false),
3403 SerializedItem::new("Terminal", 3, true, false),
3404 ],
3405 true,
3406 0,
3407 )),
3408 ],
3409 ),
3410 SerializedPaneGroup::Pane(SerializedPane::new(
3411 vec![
3412 SerializedItem::new("Terminal", 5, true, false),
3413 SerializedItem::new("Terminal", 6, false, false),
3414 ],
3415 false,
3416 0,
3417 )),
3418 ],
3419 );
3420
3421 let workspace = default_workspace(&["/tmp"], ¢er_pane);
3422
3423 db.save_workspace(workspace.clone()).await;
3424
3425 let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
3426
3427 assert_eq!(workspace.center_group, new_workspace.center_group);
3428 }
3429
3430 #[gpui::test]
3431 async fn test_cleanup_panes() {
3432 zlog::init_test();
3433
3434 let db = WorkspaceDb::open_test_db("test_cleanup_panes").await;
3435
3436 let center_pane = group(
3437 Axis::Horizontal,
3438 vec![
3439 group(
3440 Axis::Vertical,
3441 vec![
3442 SerializedPaneGroup::Pane(SerializedPane::new(
3443 vec![
3444 SerializedItem::new("Terminal", 1, false, false),
3445 SerializedItem::new("Terminal", 2, true, false),
3446 ],
3447 false,
3448 0,
3449 )),
3450 SerializedPaneGroup::Pane(SerializedPane::new(
3451 vec![
3452 SerializedItem::new("Terminal", 4, false, false),
3453 SerializedItem::new("Terminal", 3, true, false),
3454 ],
3455 true,
3456 0,
3457 )),
3458 ],
3459 ),
3460 SerializedPaneGroup::Pane(SerializedPane::new(
3461 vec![
3462 SerializedItem::new("Terminal", 5, false, false),
3463 SerializedItem::new("Terminal", 6, true, false),
3464 ],
3465 false,
3466 0,
3467 )),
3468 ],
3469 );
3470
3471 let id = &["/tmp"];
3472
3473 let mut workspace = default_workspace(id, ¢er_pane);
3474
3475 db.save_workspace(workspace.clone()).await;
3476
3477 workspace.center_group = group(
3478 Axis::Vertical,
3479 vec![
3480 SerializedPaneGroup::Pane(SerializedPane::new(
3481 vec![
3482 SerializedItem::new("Terminal", 1, false, false),
3483 SerializedItem::new("Terminal", 2, true, false),
3484 ],
3485 false,
3486 0,
3487 )),
3488 SerializedPaneGroup::Pane(SerializedPane::new(
3489 vec![
3490 SerializedItem::new("Terminal", 4, true, false),
3491 SerializedItem::new("Terminal", 3, false, false),
3492 ],
3493 true,
3494 0,
3495 )),
3496 ],
3497 );
3498
3499 db.save_workspace(workspace.clone()).await;
3500
3501 let new_workspace = db.workspace_for_roots(id).unwrap();
3502
3503 assert_eq!(workspace.center_group, new_workspace.center_group);
3504 }
3505
3506 #[gpui::test]
3507 async fn test_empty_workspace_window_bounds() {
3508 zlog::init_test();
3509
3510 let db = WorkspaceDb::open_test_db("test_empty_workspace_window_bounds").await;
3511 let id = db.next_id().await.unwrap();
3512
3513 // Create a workspace with empty paths (empty workspace)
3514 let empty_paths: &[&str] = &[];
3515 let display_uuid = Uuid::new_v4();
3516 let window_bounds = SerializedWindowBounds(WindowBounds::Windowed(Bounds {
3517 origin: point(px(100.0), px(200.0)),
3518 size: size(px(800.0), px(600.0)),
3519 }));
3520
3521 let workspace = SerializedWorkspace {
3522 id,
3523 paths: PathList::new(empty_paths),
3524 location: SerializedWorkspaceLocation::Local,
3525 center_group: Default::default(),
3526 window_bounds: None,
3527 display: None,
3528 docks: Default::default(),
3529 breakpoints: Default::default(),
3530 centered_layout: false,
3531 session_id: None,
3532 window_id: None,
3533 user_toolchains: Default::default(),
3534 };
3535
3536 // Save the workspace (this creates the record with empty paths)
3537 db.save_workspace(workspace.clone()).await;
3538
3539 // Save window bounds separately (as the actual code does via set_window_open_status)
3540 db.set_window_open_status(id, window_bounds, display_uuid)
3541 .await
3542 .unwrap();
3543
3544 // Empty workspaces cannot be retrieved by paths (they'd all match).
3545 // They must be retrieved by workspace_id.
3546 assert!(db.workspace_for_roots(empty_paths).is_none());
3547
3548 // Retrieve using workspace_for_id instead
3549 let retrieved = db.workspace_for_id(id).unwrap();
3550
3551 // Verify window bounds were persisted
3552 assert_eq!(retrieved.id, id);
3553 assert!(retrieved.window_bounds.is_some());
3554 assert_eq!(retrieved.window_bounds.unwrap().0, window_bounds.0);
3555 assert!(retrieved.display.is_some());
3556 assert_eq!(retrieved.display.unwrap(), display_uuid);
3557 }
3558}