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 mut user = None;
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 }
1453 #[cfg(any(test, feature = "test-support"))]
1454 RemoteConnectionOptions::Mock(options) => {
1455 kind = RemoteConnectionKind::Ssh;
1456 host = Some(format!("mock-{}", options.id));
1457 }
1458 }
1459 Self::get_or_create_remote_connection_query(
1460 this,
1461 kind,
1462 host,
1463 port,
1464 user,
1465 distro,
1466 name,
1467 container_id,
1468 use_podman,
1469 )
1470 }
1471
1472 fn get_or_create_remote_connection_query(
1473 this: &Connection,
1474 kind: RemoteConnectionKind,
1475 host: Option<String>,
1476 port: Option<u16>,
1477 user: Option<String>,
1478 distro: Option<String>,
1479 name: Option<String>,
1480 container_id: Option<String>,
1481 use_podman: Option<bool>,
1482 ) -> Result<RemoteConnectionId> {
1483 if let Some(id) = this.select_row_bound(sql!(
1484 SELECT id
1485 FROM remote_connections
1486 WHERE
1487 kind IS ? AND
1488 host IS ? AND
1489 port IS ? AND
1490 user IS ? AND
1491 distro IS ? AND
1492 name IS ? AND
1493 container_id IS ?
1494 LIMIT 1
1495 ))?((
1496 kind.serialize(),
1497 host.clone(),
1498 port,
1499 user.clone(),
1500 distro.clone(),
1501 name.clone(),
1502 container_id.clone(),
1503 ))? {
1504 Ok(RemoteConnectionId(id))
1505 } else {
1506 let id = this.select_row_bound(sql!(
1507 INSERT INTO remote_connections (
1508 kind,
1509 host,
1510 port,
1511 user,
1512 distro,
1513 name,
1514 container_id,
1515 use_podman
1516 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
1517 RETURNING id
1518 ))?((
1519 kind.serialize(),
1520 host,
1521 port,
1522 user,
1523 distro,
1524 name,
1525 container_id,
1526 use_podman,
1527 ))?
1528 .context("failed to insert remote project")?;
1529 Ok(RemoteConnectionId(id))
1530 }
1531 }
1532
1533 query! {
1534 pub async fn next_id() -> Result<WorkspaceId> {
1535 INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id
1536 }
1537 }
1538
1539 fn recent_workspaces(
1540 &self,
1541 ) -> Result<Vec<(WorkspaceId, PathList, Option<RemoteConnectionId>)>> {
1542 Ok(self
1543 .recent_workspaces_query()?
1544 .into_iter()
1545 .map(|(id, paths, order, remote_connection_id)| {
1546 (
1547 id,
1548 PathList::deserialize(&SerializedPathList { paths, order }),
1549 remote_connection_id.map(RemoteConnectionId),
1550 )
1551 })
1552 .collect())
1553 }
1554
1555 query! {
1556 fn recent_workspaces_query() -> Result<Vec<(WorkspaceId, String, String, Option<u64>)>> {
1557 SELECT workspace_id, paths, paths_order, remote_connection_id
1558 FROM workspaces
1559 WHERE
1560 paths IS NOT NULL OR
1561 remote_connection_id IS NOT NULL
1562 ORDER BY timestamp DESC
1563 }
1564 }
1565
1566 fn session_workspaces(
1567 &self,
1568 session_id: String,
1569 ) -> Result<
1570 Vec<(
1571 WorkspaceId,
1572 PathList,
1573 Option<u64>,
1574 Option<RemoteConnectionId>,
1575 )>,
1576 > {
1577 Ok(self
1578 .session_workspaces_query(session_id)?
1579 .into_iter()
1580 .map(
1581 |(workspace_id, paths, order, window_id, remote_connection_id)| {
1582 (
1583 WorkspaceId(workspace_id),
1584 PathList::deserialize(&SerializedPathList { paths, order }),
1585 window_id,
1586 remote_connection_id.map(RemoteConnectionId),
1587 )
1588 },
1589 )
1590 .collect())
1591 }
1592
1593 query! {
1594 fn session_workspaces_query(session_id: String) -> Result<Vec<(i64, String, String, Option<u64>, Option<u64>)>> {
1595 SELECT workspace_id, paths, paths_order, window_id, remote_connection_id
1596 FROM workspaces
1597 WHERE session_id = ?1
1598 ORDER BY timestamp DESC
1599 }
1600 }
1601
1602 query! {
1603 pub fn breakpoints_for_file(workspace_id: WorkspaceId, file_path: &Path) -> Result<Vec<Breakpoint>> {
1604 SELECT breakpoint_location
1605 FROM breakpoints
1606 WHERE workspace_id= ?1 AND path = ?2
1607 }
1608 }
1609
1610 query! {
1611 pub fn clear_breakpoints(file_path: &Path) -> Result<()> {
1612 DELETE FROM breakpoints
1613 WHERE file_path = ?2
1614 }
1615 }
1616
1617 fn remote_connections(&self) -> Result<HashMap<RemoteConnectionId, RemoteConnectionOptions>> {
1618 Ok(self.select(sql!(
1619 SELECT
1620 id, kind, host, port, user, distro, container_id, name, use_podman
1621 FROM
1622 remote_connections
1623 ))?()?
1624 .into_iter()
1625 .filter_map(
1626 |(id, kind, host, port, user, distro, container_id, name, use_podman)| {
1627 Some((
1628 RemoteConnectionId(id),
1629 Self::remote_connection_from_row(
1630 kind,
1631 host,
1632 port,
1633 user,
1634 distro,
1635 container_id,
1636 name,
1637 use_podman,
1638 )?,
1639 ))
1640 },
1641 )
1642 .collect())
1643 }
1644
1645 pub(crate) fn remote_connection(
1646 &self,
1647 id: RemoteConnectionId,
1648 ) -> Result<RemoteConnectionOptions> {
1649 let (kind, host, port, user, distro, container_id, name, use_podman) =
1650 self.select_row_bound(sql!(
1651 SELECT kind, host, port, user, distro, container_id, name, use_podman
1652 FROM remote_connections
1653 WHERE id = ?
1654 ))?(id.0)?
1655 .context("no such remote connection")?;
1656 Self::remote_connection_from_row(
1657 kind,
1658 host,
1659 port,
1660 user,
1661 distro,
1662 container_id,
1663 name,
1664 use_podman,
1665 )
1666 .context("invalid remote_connection row")
1667 }
1668
1669 fn remote_connection_from_row(
1670 kind: String,
1671 host: Option<String>,
1672 port: Option<u16>,
1673 user: Option<String>,
1674 distro: Option<String>,
1675 container_id: Option<String>,
1676 name: Option<String>,
1677 use_podman: Option<bool>,
1678 ) -> Option<RemoteConnectionOptions> {
1679 match RemoteConnectionKind::deserialize(&kind)? {
1680 RemoteConnectionKind::Wsl => Some(RemoteConnectionOptions::Wsl(WslConnectionOptions {
1681 distro_name: distro?,
1682 user: user,
1683 })),
1684 RemoteConnectionKind::Ssh => Some(RemoteConnectionOptions::Ssh(SshConnectionOptions {
1685 host: host?.into(),
1686 port,
1687 username: user,
1688 ..Default::default()
1689 })),
1690 RemoteConnectionKind::Docker => {
1691 Some(RemoteConnectionOptions::Docker(DockerConnectionOptions {
1692 container_id: container_id?,
1693 name: name?,
1694 upload_binary_over_docker_exec: false,
1695 use_podman: use_podman?,
1696 }))
1697 }
1698 }
1699 }
1700
1701 query! {
1702 pub async fn delete_workspace_by_id(id: WorkspaceId) -> Result<()> {
1703 DELETE FROM workspaces
1704 WHERE workspace_id IS ?
1705 }
1706 }
1707
1708 // Returns the recent locations which are still valid on disk and deletes ones which no longer
1709 // exist.
1710 pub async fn recent_workspaces_on_disk(
1711 &self,
1712 ) -> Result<Vec<(WorkspaceId, SerializedWorkspaceLocation, PathList)>> {
1713 let mut result = Vec::new();
1714 let mut delete_tasks = Vec::new();
1715 let remote_connections = self.remote_connections()?;
1716
1717 for (id, paths, remote_connection_id) in self.recent_workspaces()? {
1718 if let Some(remote_connection_id) = remote_connection_id {
1719 if let Some(connection_options) = remote_connections.get(&remote_connection_id) {
1720 result.push((
1721 id,
1722 SerializedWorkspaceLocation::Remote(connection_options.clone()),
1723 paths,
1724 ));
1725 } else {
1726 delete_tasks.push(self.delete_workspace_by_id(id));
1727 }
1728 continue;
1729 }
1730
1731 let has_wsl_path = if cfg!(windows) {
1732 paths
1733 .paths()
1734 .iter()
1735 .any(|path| util::paths::WslPath::from_path(path).is_some())
1736 } else {
1737 false
1738 };
1739
1740 // Delete the workspace if any of the paths are WSL paths.
1741 // If a local workspace points to WSL, this check will cause us to wait for the
1742 // WSL VM and file server to boot up. This can block for many seconds.
1743 // Supported scenarios use remote workspaces.
1744 if !has_wsl_path && paths.paths().iter().all(|path| path.exists()) {
1745 // Only show directories in recent projects
1746 if paths.paths().iter().any(|path| path.is_dir()) {
1747 result.push((id, SerializedWorkspaceLocation::Local, paths));
1748 }
1749 } else {
1750 delete_tasks.push(self.delete_workspace_by_id(id));
1751 }
1752 }
1753
1754 futures::future::join_all(delete_tasks).await;
1755 Ok(result)
1756 }
1757
1758 pub async fn last_workspace(
1759 &self,
1760 ) -> Result<Option<(WorkspaceId, SerializedWorkspaceLocation, PathList)>> {
1761 Ok(self.recent_workspaces_on_disk().await?.into_iter().next())
1762 }
1763
1764 // Returns the locations of the workspaces that were still opened when the last
1765 // session was closed (i.e. when Zed was quit).
1766 // If `last_session_window_order` is provided, the returned locations are ordered
1767 // according to that.
1768 pub fn last_session_workspace_locations(
1769 &self,
1770 last_session_id: &str,
1771 last_session_window_stack: Option<Vec<WindowId>>,
1772 ) -> Result<Vec<(WorkspaceId, SerializedWorkspaceLocation, PathList)>> {
1773 let mut workspaces = Vec::new();
1774
1775 for (workspace_id, paths, window_id, remote_connection_id) in
1776 self.session_workspaces(last_session_id.to_owned())?
1777 {
1778 if let Some(remote_connection_id) = remote_connection_id {
1779 workspaces.push((
1780 workspace_id,
1781 SerializedWorkspaceLocation::Remote(
1782 self.remote_connection(remote_connection_id)?,
1783 ),
1784 paths,
1785 window_id.map(WindowId::from),
1786 ));
1787 } else if paths.is_empty() {
1788 // Empty workspace with items (drafts, files) - include for restoration
1789 workspaces.push((
1790 workspace_id,
1791 SerializedWorkspaceLocation::Local,
1792 paths,
1793 window_id.map(WindowId::from),
1794 ));
1795 } else if paths.paths().iter().all(|path| path.exists())
1796 && paths.paths().iter().any(|path| path.is_dir())
1797 {
1798 workspaces.push((
1799 workspace_id,
1800 SerializedWorkspaceLocation::Local,
1801 paths,
1802 window_id.map(WindowId::from),
1803 ));
1804 }
1805 }
1806
1807 if let Some(stack) = last_session_window_stack {
1808 workspaces.sort_by_key(|(_, _, _, window_id)| {
1809 window_id
1810 .and_then(|id| stack.iter().position(|&order_id| order_id == id))
1811 .unwrap_or(usize::MAX)
1812 });
1813 }
1814
1815 Ok(workspaces
1816 .into_iter()
1817 .map(|(workspace_id, location, paths, _)| (workspace_id, location, paths))
1818 .collect::<Vec<_>>())
1819 }
1820
1821 fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
1822 Ok(self
1823 .get_pane_group(workspace_id, None)?
1824 .into_iter()
1825 .next()
1826 .unwrap_or_else(|| {
1827 SerializedPaneGroup::Pane(SerializedPane {
1828 active: true,
1829 children: vec![],
1830 pinned_count: 0,
1831 })
1832 }))
1833 }
1834
1835 fn get_pane_group(
1836 &self,
1837 workspace_id: WorkspaceId,
1838 group_id: Option<GroupId>,
1839 ) -> Result<Vec<SerializedPaneGroup>> {
1840 type GroupKey = (Option<GroupId>, WorkspaceId);
1841 type GroupOrPane = (
1842 Option<GroupId>,
1843 Option<SerializedAxis>,
1844 Option<PaneId>,
1845 Option<bool>,
1846 Option<usize>,
1847 Option<String>,
1848 );
1849 self.select_bound::<GroupKey, GroupOrPane>(sql!(
1850 SELECT group_id, axis, pane_id, active, pinned_count, flexes
1851 FROM (SELECT
1852 group_id,
1853 axis,
1854 NULL as pane_id,
1855 NULL as active,
1856 NULL as pinned_count,
1857 position,
1858 parent_group_id,
1859 workspace_id,
1860 flexes
1861 FROM pane_groups
1862 UNION
1863 SELECT
1864 NULL,
1865 NULL,
1866 center_panes.pane_id,
1867 panes.active as active,
1868 pinned_count,
1869 position,
1870 parent_group_id,
1871 panes.workspace_id as workspace_id,
1872 NULL
1873 FROM center_panes
1874 JOIN panes ON center_panes.pane_id = panes.pane_id)
1875 WHERE parent_group_id IS ? AND workspace_id = ?
1876 ORDER BY position
1877 ))?((group_id, workspace_id))?
1878 .into_iter()
1879 .map(|(group_id, axis, pane_id, active, pinned_count, flexes)| {
1880 let maybe_pane = maybe!({ Some((pane_id?, active?, pinned_count?)) });
1881 if let Some((group_id, axis)) = group_id.zip(axis) {
1882 let flexes = flexes
1883 .map(|flexes: String| serde_json::from_str::<Vec<f32>>(&flexes))
1884 .transpose()?;
1885
1886 Ok(SerializedPaneGroup::Group {
1887 axis,
1888 children: self.get_pane_group(workspace_id, Some(group_id))?,
1889 flexes,
1890 })
1891 } else if let Some((pane_id, active, pinned_count)) = maybe_pane {
1892 Ok(SerializedPaneGroup::Pane(SerializedPane::new(
1893 self.get_items(pane_id)?,
1894 active,
1895 pinned_count,
1896 )))
1897 } else {
1898 bail!("Pane Group Child was neither a pane group or a pane");
1899 }
1900 })
1901 // Filter out panes and pane groups which don't have any children or items
1902 .filter(|pane_group| match pane_group {
1903 Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
1904 Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
1905 _ => true,
1906 })
1907 .collect::<Result<_>>()
1908 }
1909
1910 fn save_pane_group(
1911 conn: &Connection,
1912 workspace_id: WorkspaceId,
1913 pane_group: &SerializedPaneGroup,
1914 parent: Option<(GroupId, usize)>,
1915 ) -> Result<()> {
1916 if parent.is_none() {
1917 log::debug!("Saving a pane group for workspace {workspace_id:?}");
1918 }
1919 match pane_group {
1920 SerializedPaneGroup::Group {
1921 axis,
1922 children,
1923 flexes,
1924 } => {
1925 let (parent_id, position) = parent.unzip();
1926
1927 let flex_string = flexes
1928 .as_ref()
1929 .map(|flexes| serde_json::json!(flexes).to_string());
1930
1931 let group_id = conn.select_row_bound::<_, i64>(sql!(
1932 INSERT INTO pane_groups(
1933 workspace_id,
1934 parent_group_id,
1935 position,
1936 axis,
1937 flexes
1938 )
1939 VALUES (?, ?, ?, ?, ?)
1940 RETURNING group_id
1941 ))?((
1942 workspace_id,
1943 parent_id,
1944 position,
1945 *axis,
1946 flex_string,
1947 ))?
1948 .context("Couldn't retrieve group_id from inserted pane_group")?;
1949
1950 for (position, group) in children.iter().enumerate() {
1951 Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))?
1952 }
1953
1954 Ok(())
1955 }
1956 SerializedPaneGroup::Pane(pane) => {
1957 Self::save_pane(conn, workspace_id, pane, parent)?;
1958 Ok(())
1959 }
1960 }
1961 }
1962
1963 fn save_pane(
1964 conn: &Connection,
1965 workspace_id: WorkspaceId,
1966 pane: &SerializedPane,
1967 parent: Option<(GroupId, usize)>,
1968 ) -> Result<PaneId> {
1969 let pane_id = conn.select_row_bound::<_, i64>(sql!(
1970 INSERT INTO panes(workspace_id, active, pinned_count)
1971 VALUES (?, ?, ?)
1972 RETURNING pane_id
1973 ))?((workspace_id, pane.active, pane.pinned_count))?
1974 .context("Could not retrieve inserted pane_id")?;
1975
1976 let (parent_id, order) = parent.unzip();
1977 conn.exec_bound(sql!(
1978 INSERT INTO center_panes(pane_id, parent_group_id, position)
1979 VALUES (?, ?, ?)
1980 ))?((pane_id, parent_id, order))?;
1981
1982 Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
1983
1984 Ok(pane_id)
1985 }
1986
1987 fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
1988 self.select_bound(sql!(
1989 SELECT kind, item_id, active, preview FROM items
1990 WHERE pane_id = ?
1991 ORDER BY position
1992 ))?(pane_id)
1993 }
1994
1995 fn save_items(
1996 conn: &Connection,
1997 workspace_id: WorkspaceId,
1998 pane_id: PaneId,
1999 items: &[SerializedItem],
2000 ) -> Result<()> {
2001 let mut insert = conn.exec_bound(sql!(
2002 INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active, preview) VALUES (?, ?, ?, ?, ?, ?, ?)
2003 )).context("Preparing insertion")?;
2004 for (position, item) in items.iter().enumerate() {
2005 insert((workspace_id, pane_id, position, item))?;
2006 }
2007
2008 Ok(())
2009 }
2010
2011 query! {
2012 pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> {
2013 UPDATE workspaces
2014 SET timestamp = CURRENT_TIMESTAMP
2015 WHERE workspace_id = ?
2016 }
2017 }
2018
2019 query! {
2020 pub(crate) async fn set_window_open_status(workspace_id: WorkspaceId, bounds: SerializedWindowBounds, display: Uuid) -> Result<()> {
2021 UPDATE workspaces
2022 SET window_state = ?2,
2023 window_x = ?3,
2024 window_y = ?4,
2025 window_width = ?5,
2026 window_height = ?6,
2027 display = ?7
2028 WHERE workspace_id = ?1
2029 }
2030 }
2031
2032 query! {
2033 pub(crate) async fn set_centered_layout(workspace_id: WorkspaceId, centered_layout: bool) -> Result<()> {
2034 UPDATE workspaces
2035 SET centered_layout = ?2
2036 WHERE workspace_id = ?1
2037 }
2038 }
2039
2040 query! {
2041 pub(crate) async fn set_session_id(workspace_id: WorkspaceId, session_id: Option<String>) -> Result<()> {
2042 UPDATE workspaces
2043 SET session_id = ?2
2044 WHERE workspace_id = ?1
2045 }
2046 }
2047
2048 pub(crate) async fn toolchains(
2049 &self,
2050 workspace_id: WorkspaceId,
2051 ) -> Result<Vec<(Toolchain, Arc<Path>, Arc<RelPath>)>> {
2052 self.write(move |this| {
2053 let mut select = this
2054 .select_bound(sql!(
2055 SELECT
2056 name, path, worktree_root_path, relative_worktree_path, language_name, raw_json
2057 FROM toolchains
2058 WHERE workspace_id = ?
2059 ))
2060 .context("select toolchains")?;
2061
2062 let toolchain: Vec<(String, String, String, String, String, String)> =
2063 select(workspace_id)?;
2064
2065 Ok(toolchain
2066 .into_iter()
2067 .filter_map(
2068 |(name, path, worktree_root_path, relative_worktree_path, language, json)| {
2069 Some((
2070 Toolchain {
2071 name: name.into(),
2072 path: path.into(),
2073 language_name: LanguageName::new(&language),
2074 as_json: serde_json::Value::from_str(&json).ok()?,
2075 },
2076 Arc::from(worktree_root_path.as_ref()),
2077 RelPath::from_proto(&relative_worktree_path).log_err()?,
2078 ))
2079 },
2080 )
2081 .collect())
2082 })
2083 .await
2084 }
2085
2086 pub async fn set_toolchain(
2087 &self,
2088 workspace_id: WorkspaceId,
2089 worktree_root_path: Arc<Path>,
2090 relative_worktree_path: Arc<RelPath>,
2091 toolchain: Toolchain,
2092 ) -> Result<()> {
2093 log::debug!(
2094 "Setting toolchain for workspace, worktree: {worktree_root_path:?}, relative path: {relative_worktree_path:?}, toolchain: {}",
2095 toolchain.name
2096 );
2097 self.write(move |conn| {
2098 let mut insert = conn
2099 .exec_bound(sql!(
2100 INSERT INTO toolchains(workspace_id, worktree_root_path, relative_worktree_path, language_name, name, path, raw_json) VALUES (?, ?, ?, ?, ?, ?, ?)
2101 ON CONFLICT DO
2102 UPDATE SET
2103 name = ?5,
2104 path = ?6,
2105 raw_json = ?7
2106 ))
2107 .context("Preparing insertion")?;
2108
2109 insert((
2110 workspace_id,
2111 worktree_root_path.to_string_lossy().into_owned(),
2112 relative_worktree_path.as_unix_str(),
2113 toolchain.language_name.as_ref(),
2114 toolchain.name.as_ref(),
2115 toolchain.path.as_ref(),
2116 toolchain.as_json.to_string(),
2117 ))?;
2118
2119 Ok(())
2120 }).await
2121 }
2122
2123 pub(crate) async fn save_trusted_worktrees(
2124 &self,
2125 trusted_worktrees: HashMap<Option<RemoteHostLocation>, HashSet<PathBuf>>,
2126 ) -> anyhow::Result<()> {
2127 use anyhow::Context as _;
2128 use db::sqlez::statement::Statement;
2129 use itertools::Itertools as _;
2130
2131 DB.clear_trusted_worktrees()
2132 .await
2133 .context("clearing previous trust state")?;
2134
2135 let trusted_worktrees = trusted_worktrees
2136 .into_iter()
2137 .flat_map(|(host, abs_paths)| {
2138 abs_paths
2139 .into_iter()
2140 .map(move |abs_path| (Some(abs_path), host.clone()))
2141 })
2142 .collect::<Vec<_>>();
2143 let mut first_worktree;
2144 let mut last_worktree = 0_usize;
2145 for (count, placeholders) in std::iter::once("(?, ?, ?)")
2146 .cycle()
2147 .take(trusted_worktrees.len())
2148 .chunks(MAX_QUERY_PLACEHOLDERS / 3)
2149 .into_iter()
2150 .map(|chunk| {
2151 let mut count = 0;
2152 let placeholders = chunk
2153 .inspect(|_| {
2154 count += 1;
2155 })
2156 .join(", ");
2157 (count, placeholders)
2158 })
2159 .collect::<Vec<_>>()
2160 {
2161 first_worktree = last_worktree;
2162 last_worktree = last_worktree + count;
2163 let query = format!(
2164 r#"INSERT INTO trusted_worktrees(absolute_path, user_name, host_name)
2165VALUES {placeholders};"#
2166 );
2167
2168 let trusted_worktrees = trusted_worktrees[first_worktree..last_worktree].to_vec();
2169 self.write(move |conn| {
2170 let mut statement = Statement::prepare(conn, query)?;
2171 let mut next_index = 1;
2172 for (abs_path, host) in trusted_worktrees {
2173 let abs_path = abs_path.as_ref().map(|abs_path| abs_path.to_string_lossy());
2174 next_index = statement.bind(
2175 &abs_path.as_ref().map(|abs_path| abs_path.as_ref()),
2176 next_index,
2177 )?;
2178 next_index = statement.bind(
2179 &host
2180 .as_ref()
2181 .and_then(|host| Some(host.user_name.as_ref()?.as_str())),
2182 next_index,
2183 )?;
2184 next_index = statement.bind(
2185 &host.as_ref().map(|host| host.host_identifier.as_str()),
2186 next_index,
2187 )?;
2188 }
2189 statement.exec()
2190 })
2191 .await
2192 .context("inserting new trusted state")?;
2193 }
2194 Ok(())
2195 }
2196
2197 pub fn fetch_trusted_worktrees(&self) -> Result<DbTrustedPaths> {
2198 let trusted_worktrees = DB.trusted_worktrees()?;
2199 Ok(trusted_worktrees
2200 .into_iter()
2201 .filter_map(|(abs_path, user_name, host_name)| {
2202 let db_host = match (user_name, host_name) {
2203 (None, Some(host_name)) => Some(RemoteHostLocation {
2204 user_name: None,
2205 host_identifier: SharedString::new(host_name),
2206 }),
2207 (Some(user_name), Some(host_name)) => Some(RemoteHostLocation {
2208 user_name: Some(SharedString::new(user_name)),
2209 host_identifier: SharedString::new(host_name),
2210 }),
2211 _ => None,
2212 };
2213 Some((db_host, abs_path?))
2214 })
2215 .fold(HashMap::default(), |mut acc, (remote_host, abs_path)| {
2216 acc.entry(remote_host)
2217 .or_insert_with(HashSet::default)
2218 .insert(abs_path);
2219 acc
2220 }))
2221 }
2222
2223 query! {
2224 fn trusted_worktrees() -> Result<Vec<(Option<PathBuf>, Option<String>, Option<String>)>> {
2225 SELECT absolute_path, user_name, host_name
2226 FROM trusted_worktrees
2227 }
2228 }
2229
2230 query! {
2231 pub async fn clear_trusted_worktrees() -> Result<()> {
2232 DELETE FROM trusted_worktrees
2233 }
2234 }
2235}
2236
2237pub fn delete_unloaded_items(
2238 alive_items: Vec<ItemId>,
2239 workspace_id: WorkspaceId,
2240 table: &'static str,
2241 db: &ThreadSafeConnection,
2242 cx: &mut App,
2243) -> Task<Result<()>> {
2244 let db = db.clone();
2245 cx.spawn(async move |_| {
2246 let placeholders = alive_items
2247 .iter()
2248 .map(|_| "?")
2249 .collect::<Vec<&str>>()
2250 .join(", ");
2251
2252 let query = format!(
2253 "DELETE FROM {table} WHERE workspace_id = ? AND item_id NOT IN ({placeholders})"
2254 );
2255
2256 db.write(move |conn| {
2257 let mut statement = Statement::prepare(conn, query)?;
2258 let mut next_index = statement.bind(&workspace_id, 1)?;
2259 for id in alive_items {
2260 next_index = statement.bind(&id, next_index)?;
2261 }
2262 statement.exec()
2263 })
2264 .await
2265 })
2266}
2267
2268#[cfg(test)]
2269mod tests {
2270 use super::*;
2271 use crate::persistence::model::{
2272 SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
2273 };
2274 use gpui;
2275 use pretty_assertions::assert_eq;
2276 use remote::SshConnectionOptions;
2277 use std::{thread, time::Duration};
2278
2279 #[gpui::test]
2280 async fn test_breakpoints() {
2281 zlog::init_test();
2282
2283 let db = WorkspaceDb::open_test_db("test_breakpoints").await;
2284 let id = db.next_id().await.unwrap();
2285
2286 let path = Path::new("/tmp/test.rs");
2287
2288 let breakpoint = Breakpoint {
2289 position: 123,
2290 message: None,
2291 state: BreakpointState::Enabled,
2292 condition: None,
2293 hit_condition: None,
2294 };
2295
2296 let log_breakpoint = Breakpoint {
2297 position: 456,
2298 message: Some("Test log message".into()),
2299 state: BreakpointState::Enabled,
2300 condition: None,
2301 hit_condition: None,
2302 };
2303
2304 let disable_breakpoint = Breakpoint {
2305 position: 578,
2306 message: None,
2307 state: BreakpointState::Disabled,
2308 condition: None,
2309 hit_condition: None,
2310 };
2311
2312 let condition_breakpoint = Breakpoint {
2313 position: 789,
2314 message: None,
2315 state: BreakpointState::Enabled,
2316 condition: Some("x > 5".into()),
2317 hit_condition: None,
2318 };
2319
2320 let hit_condition_breakpoint = Breakpoint {
2321 position: 999,
2322 message: None,
2323 state: BreakpointState::Enabled,
2324 condition: None,
2325 hit_condition: Some(">= 3".into()),
2326 };
2327
2328 let workspace = SerializedWorkspace {
2329 id,
2330 paths: PathList::new(&["/tmp"]),
2331 location: SerializedWorkspaceLocation::Local,
2332 center_group: Default::default(),
2333 window_bounds: Default::default(),
2334 display: Default::default(),
2335 docks: Default::default(),
2336 centered_layout: false,
2337 breakpoints: {
2338 let mut map = collections::BTreeMap::default();
2339 map.insert(
2340 Arc::from(path),
2341 vec![
2342 SourceBreakpoint {
2343 row: breakpoint.position,
2344 path: Arc::from(path),
2345 message: breakpoint.message.clone(),
2346 state: breakpoint.state,
2347 condition: breakpoint.condition.clone(),
2348 hit_condition: breakpoint.hit_condition.clone(),
2349 },
2350 SourceBreakpoint {
2351 row: log_breakpoint.position,
2352 path: Arc::from(path),
2353 message: log_breakpoint.message.clone(),
2354 state: log_breakpoint.state,
2355 condition: log_breakpoint.condition.clone(),
2356 hit_condition: log_breakpoint.hit_condition.clone(),
2357 },
2358 SourceBreakpoint {
2359 row: disable_breakpoint.position,
2360 path: Arc::from(path),
2361 message: disable_breakpoint.message.clone(),
2362 state: disable_breakpoint.state,
2363 condition: disable_breakpoint.condition.clone(),
2364 hit_condition: disable_breakpoint.hit_condition.clone(),
2365 },
2366 SourceBreakpoint {
2367 row: condition_breakpoint.position,
2368 path: Arc::from(path),
2369 message: condition_breakpoint.message.clone(),
2370 state: condition_breakpoint.state,
2371 condition: condition_breakpoint.condition.clone(),
2372 hit_condition: condition_breakpoint.hit_condition.clone(),
2373 },
2374 SourceBreakpoint {
2375 row: hit_condition_breakpoint.position,
2376 path: Arc::from(path),
2377 message: hit_condition_breakpoint.message.clone(),
2378 state: hit_condition_breakpoint.state,
2379 condition: hit_condition_breakpoint.condition.clone(),
2380 hit_condition: hit_condition_breakpoint.hit_condition.clone(),
2381 },
2382 ],
2383 );
2384 map
2385 },
2386 session_id: None,
2387 window_id: None,
2388 user_toolchains: Default::default(),
2389 };
2390
2391 db.save_workspace(workspace.clone()).await;
2392
2393 let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
2394 let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(path)).unwrap();
2395
2396 assert_eq!(loaded_breakpoints.len(), 5);
2397
2398 // normal breakpoint
2399 assert_eq!(loaded_breakpoints[0].row, breakpoint.position);
2400 assert_eq!(loaded_breakpoints[0].message, breakpoint.message);
2401 assert_eq!(loaded_breakpoints[0].condition, breakpoint.condition);
2402 assert_eq!(
2403 loaded_breakpoints[0].hit_condition,
2404 breakpoint.hit_condition
2405 );
2406 assert_eq!(loaded_breakpoints[0].state, breakpoint.state);
2407 assert_eq!(loaded_breakpoints[0].path, Arc::from(path));
2408
2409 // enabled breakpoint
2410 assert_eq!(loaded_breakpoints[1].row, log_breakpoint.position);
2411 assert_eq!(loaded_breakpoints[1].message, log_breakpoint.message);
2412 assert_eq!(loaded_breakpoints[1].condition, log_breakpoint.condition);
2413 assert_eq!(
2414 loaded_breakpoints[1].hit_condition,
2415 log_breakpoint.hit_condition
2416 );
2417 assert_eq!(loaded_breakpoints[1].state, log_breakpoint.state);
2418 assert_eq!(loaded_breakpoints[1].path, Arc::from(path));
2419
2420 // disable breakpoint
2421 assert_eq!(loaded_breakpoints[2].row, disable_breakpoint.position);
2422 assert_eq!(loaded_breakpoints[2].message, disable_breakpoint.message);
2423 assert_eq!(
2424 loaded_breakpoints[2].condition,
2425 disable_breakpoint.condition
2426 );
2427 assert_eq!(
2428 loaded_breakpoints[2].hit_condition,
2429 disable_breakpoint.hit_condition
2430 );
2431 assert_eq!(loaded_breakpoints[2].state, disable_breakpoint.state);
2432 assert_eq!(loaded_breakpoints[2].path, Arc::from(path));
2433
2434 // condition breakpoint
2435 assert_eq!(loaded_breakpoints[3].row, condition_breakpoint.position);
2436 assert_eq!(loaded_breakpoints[3].message, condition_breakpoint.message);
2437 assert_eq!(
2438 loaded_breakpoints[3].condition,
2439 condition_breakpoint.condition
2440 );
2441 assert_eq!(
2442 loaded_breakpoints[3].hit_condition,
2443 condition_breakpoint.hit_condition
2444 );
2445 assert_eq!(loaded_breakpoints[3].state, condition_breakpoint.state);
2446 assert_eq!(loaded_breakpoints[3].path, Arc::from(path));
2447
2448 // hit condition breakpoint
2449 assert_eq!(loaded_breakpoints[4].row, hit_condition_breakpoint.position);
2450 assert_eq!(
2451 loaded_breakpoints[4].message,
2452 hit_condition_breakpoint.message
2453 );
2454 assert_eq!(
2455 loaded_breakpoints[4].condition,
2456 hit_condition_breakpoint.condition
2457 );
2458 assert_eq!(
2459 loaded_breakpoints[4].hit_condition,
2460 hit_condition_breakpoint.hit_condition
2461 );
2462 assert_eq!(loaded_breakpoints[4].state, hit_condition_breakpoint.state);
2463 assert_eq!(loaded_breakpoints[4].path, Arc::from(path));
2464 }
2465
2466 #[gpui::test]
2467 async fn test_remove_last_breakpoint() {
2468 zlog::init_test();
2469
2470 let db = WorkspaceDb::open_test_db("test_remove_last_breakpoint").await;
2471 let id = db.next_id().await.unwrap();
2472
2473 let singular_path = Path::new("/tmp/test_remove_last_breakpoint.rs");
2474
2475 let breakpoint_to_remove = Breakpoint {
2476 position: 100,
2477 message: None,
2478 state: BreakpointState::Enabled,
2479 condition: None,
2480 hit_condition: None,
2481 };
2482
2483 let workspace = SerializedWorkspace {
2484 id,
2485 paths: PathList::new(&["/tmp"]),
2486 location: SerializedWorkspaceLocation::Local,
2487 center_group: Default::default(),
2488 window_bounds: Default::default(),
2489 display: Default::default(),
2490 docks: Default::default(),
2491 centered_layout: false,
2492 breakpoints: {
2493 let mut map = collections::BTreeMap::default();
2494 map.insert(
2495 Arc::from(singular_path),
2496 vec![SourceBreakpoint {
2497 row: breakpoint_to_remove.position,
2498 path: Arc::from(singular_path),
2499 message: None,
2500 state: BreakpointState::Enabled,
2501 condition: None,
2502 hit_condition: None,
2503 }],
2504 );
2505 map
2506 },
2507 session_id: None,
2508 window_id: None,
2509 user_toolchains: Default::default(),
2510 };
2511
2512 db.save_workspace(workspace.clone()).await;
2513
2514 let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
2515 let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(singular_path)).unwrap();
2516
2517 assert_eq!(loaded_breakpoints.len(), 1);
2518 assert_eq!(loaded_breakpoints[0].row, breakpoint_to_remove.position);
2519 assert_eq!(loaded_breakpoints[0].message, breakpoint_to_remove.message);
2520 assert_eq!(
2521 loaded_breakpoints[0].condition,
2522 breakpoint_to_remove.condition
2523 );
2524 assert_eq!(
2525 loaded_breakpoints[0].hit_condition,
2526 breakpoint_to_remove.hit_condition
2527 );
2528 assert_eq!(loaded_breakpoints[0].state, breakpoint_to_remove.state);
2529 assert_eq!(loaded_breakpoints[0].path, Arc::from(singular_path));
2530
2531 let workspace_without_breakpoint = SerializedWorkspace {
2532 id,
2533 paths: PathList::new(&["/tmp"]),
2534 location: SerializedWorkspaceLocation::Local,
2535 center_group: Default::default(),
2536 window_bounds: Default::default(),
2537 display: Default::default(),
2538 docks: Default::default(),
2539 centered_layout: false,
2540 breakpoints: collections::BTreeMap::default(),
2541 session_id: None,
2542 window_id: None,
2543 user_toolchains: Default::default(),
2544 };
2545
2546 db.save_workspace(workspace_without_breakpoint.clone())
2547 .await;
2548
2549 let loaded_after_remove = db.workspace_for_roots(&["/tmp"]).unwrap();
2550 let empty_breakpoints = loaded_after_remove
2551 .breakpoints
2552 .get(&Arc::from(singular_path));
2553
2554 assert!(empty_breakpoints.is_none());
2555 }
2556
2557 #[gpui::test]
2558 async fn test_next_id_stability() {
2559 zlog::init_test();
2560
2561 let db = WorkspaceDb::open_test_db("test_next_id_stability").await;
2562
2563 db.write(|conn| {
2564 conn.migrate(
2565 "test_table",
2566 &[sql!(
2567 CREATE TABLE test_table(
2568 text TEXT,
2569 workspace_id INTEGER,
2570 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
2571 ON DELETE CASCADE
2572 ) STRICT;
2573 )],
2574 |_, _, _| false,
2575 )
2576 .unwrap();
2577 })
2578 .await;
2579
2580 let id = db.next_id().await.unwrap();
2581 // Assert the empty row got inserted
2582 assert_eq!(
2583 Some(id),
2584 db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
2585 SELECT workspace_id FROM workspaces WHERE workspace_id = ?
2586 ))
2587 .unwrap()(id)
2588 .unwrap()
2589 );
2590
2591 db.write(move |conn| {
2592 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2593 .unwrap()(("test-text-1", id))
2594 .unwrap()
2595 })
2596 .await;
2597
2598 let test_text_1 = db
2599 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2600 .unwrap()(1)
2601 .unwrap()
2602 .unwrap();
2603 assert_eq!(test_text_1, "test-text-1");
2604 }
2605
2606 #[gpui::test]
2607 async fn test_workspace_id_stability() {
2608 zlog::init_test();
2609
2610 let db = WorkspaceDb::open_test_db("test_workspace_id_stability").await;
2611
2612 db.write(|conn| {
2613 conn.migrate(
2614 "test_table",
2615 &[sql!(
2616 CREATE TABLE test_table(
2617 text TEXT,
2618 workspace_id INTEGER,
2619 FOREIGN KEY(workspace_id)
2620 REFERENCES workspaces(workspace_id)
2621 ON DELETE CASCADE
2622 ) STRICT;)],
2623 |_, _, _| false,
2624 )
2625 })
2626 .await
2627 .unwrap();
2628
2629 let mut workspace_1 = SerializedWorkspace {
2630 id: WorkspaceId(1),
2631 paths: PathList::new(&["/tmp", "/tmp2"]),
2632 location: SerializedWorkspaceLocation::Local,
2633 center_group: Default::default(),
2634 window_bounds: Default::default(),
2635 display: Default::default(),
2636 docks: Default::default(),
2637 centered_layout: false,
2638 breakpoints: Default::default(),
2639 session_id: None,
2640 window_id: None,
2641 user_toolchains: Default::default(),
2642 };
2643
2644 let workspace_2 = SerializedWorkspace {
2645 id: WorkspaceId(2),
2646 paths: PathList::new(&["/tmp"]),
2647 location: SerializedWorkspaceLocation::Local,
2648 center_group: Default::default(),
2649 window_bounds: Default::default(),
2650 display: Default::default(),
2651 docks: Default::default(),
2652 centered_layout: false,
2653 breakpoints: Default::default(),
2654 session_id: None,
2655 window_id: None,
2656 user_toolchains: Default::default(),
2657 };
2658
2659 db.save_workspace(workspace_1.clone()).await;
2660
2661 db.write(|conn| {
2662 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2663 .unwrap()(("test-text-1", 1))
2664 .unwrap();
2665 })
2666 .await;
2667
2668 db.save_workspace(workspace_2.clone()).await;
2669
2670 db.write(|conn| {
2671 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2672 .unwrap()(("test-text-2", 2))
2673 .unwrap();
2674 })
2675 .await;
2676
2677 workspace_1.paths = PathList::new(&["/tmp", "/tmp3"]);
2678 db.save_workspace(workspace_1.clone()).await;
2679 db.save_workspace(workspace_1).await;
2680 db.save_workspace(workspace_2).await;
2681
2682 let test_text_2 = db
2683 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2684 .unwrap()(2)
2685 .unwrap()
2686 .unwrap();
2687 assert_eq!(test_text_2, "test-text-2");
2688
2689 let test_text_1 = db
2690 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2691 .unwrap()(1)
2692 .unwrap()
2693 .unwrap();
2694 assert_eq!(test_text_1, "test-text-1");
2695 }
2696
2697 fn group(axis: Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
2698 SerializedPaneGroup::Group {
2699 axis: SerializedAxis(axis),
2700 flexes: None,
2701 children,
2702 }
2703 }
2704
2705 #[gpui::test]
2706 async fn test_full_workspace_serialization() {
2707 zlog::init_test();
2708
2709 let db = WorkspaceDb::open_test_db("test_full_workspace_serialization").await;
2710
2711 // -----------------
2712 // | 1,2 | 5,6 |
2713 // | - - - | |
2714 // | 3,4 | |
2715 // -----------------
2716 let center_group = group(
2717 Axis::Horizontal,
2718 vec![
2719 group(
2720 Axis::Vertical,
2721 vec![
2722 SerializedPaneGroup::Pane(SerializedPane::new(
2723 vec![
2724 SerializedItem::new("Terminal", 5, false, false),
2725 SerializedItem::new("Terminal", 6, true, false),
2726 ],
2727 false,
2728 0,
2729 )),
2730 SerializedPaneGroup::Pane(SerializedPane::new(
2731 vec![
2732 SerializedItem::new("Terminal", 7, true, false),
2733 SerializedItem::new("Terminal", 8, false, false),
2734 ],
2735 false,
2736 0,
2737 )),
2738 ],
2739 ),
2740 SerializedPaneGroup::Pane(SerializedPane::new(
2741 vec![
2742 SerializedItem::new("Terminal", 9, false, false),
2743 SerializedItem::new("Terminal", 10, true, false),
2744 ],
2745 false,
2746 0,
2747 )),
2748 ],
2749 );
2750
2751 let workspace = SerializedWorkspace {
2752 id: WorkspaceId(5),
2753 paths: PathList::new(&["/tmp", "/tmp2"]),
2754 location: SerializedWorkspaceLocation::Local,
2755 center_group,
2756 window_bounds: Default::default(),
2757 breakpoints: Default::default(),
2758 display: Default::default(),
2759 docks: Default::default(),
2760 centered_layout: false,
2761 session_id: None,
2762 window_id: Some(999),
2763 user_toolchains: Default::default(),
2764 };
2765
2766 db.save_workspace(workspace.clone()).await;
2767
2768 let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
2769 assert_eq!(workspace, round_trip_workspace.unwrap());
2770
2771 // Test guaranteed duplicate IDs
2772 db.save_workspace(workspace.clone()).await;
2773 db.save_workspace(workspace.clone()).await;
2774
2775 let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
2776 assert_eq!(workspace, round_trip_workspace.unwrap());
2777 }
2778
2779 #[gpui::test]
2780 async fn test_workspace_assignment() {
2781 zlog::init_test();
2782
2783 let db = WorkspaceDb::open_test_db("test_basic_functionality").await;
2784
2785 let workspace_1 = SerializedWorkspace {
2786 id: WorkspaceId(1),
2787 paths: PathList::new(&["/tmp", "/tmp2"]),
2788 location: SerializedWorkspaceLocation::Local,
2789 center_group: Default::default(),
2790 window_bounds: Default::default(),
2791 breakpoints: Default::default(),
2792 display: Default::default(),
2793 docks: Default::default(),
2794 centered_layout: false,
2795 session_id: None,
2796 window_id: Some(1),
2797 user_toolchains: Default::default(),
2798 };
2799
2800 let mut workspace_2 = SerializedWorkspace {
2801 id: WorkspaceId(2),
2802 paths: PathList::new(&["/tmp"]),
2803 location: SerializedWorkspaceLocation::Local,
2804 center_group: Default::default(),
2805 window_bounds: Default::default(),
2806 display: Default::default(),
2807 docks: Default::default(),
2808 centered_layout: false,
2809 breakpoints: Default::default(),
2810 session_id: None,
2811 window_id: Some(2),
2812 user_toolchains: Default::default(),
2813 };
2814
2815 db.save_workspace(workspace_1.clone()).await;
2816 db.save_workspace(workspace_2.clone()).await;
2817
2818 // Test that paths are treated as a set
2819 assert_eq!(
2820 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2821 workspace_1
2822 );
2823 assert_eq!(
2824 db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
2825 workspace_1
2826 );
2827
2828 // Make sure that other keys work
2829 assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
2830 assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
2831
2832 // Test 'mutate' case of updating a pre-existing id
2833 workspace_2.paths = PathList::new(&["/tmp", "/tmp2"]);
2834
2835 db.save_workspace(workspace_2.clone()).await;
2836 assert_eq!(
2837 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2838 workspace_2
2839 );
2840
2841 // Test other mechanism for mutating
2842 let mut workspace_3 = SerializedWorkspace {
2843 id: WorkspaceId(3),
2844 paths: PathList::new(&["/tmp2", "/tmp"]),
2845 location: SerializedWorkspaceLocation::Local,
2846 center_group: Default::default(),
2847 window_bounds: Default::default(),
2848 breakpoints: Default::default(),
2849 display: Default::default(),
2850 docks: Default::default(),
2851 centered_layout: false,
2852 session_id: None,
2853 window_id: Some(3),
2854 user_toolchains: Default::default(),
2855 };
2856
2857 db.save_workspace(workspace_3.clone()).await;
2858 assert_eq!(
2859 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2860 workspace_3
2861 );
2862
2863 // Make sure that updating paths differently also works
2864 workspace_3.paths = PathList::new(&["/tmp3", "/tmp4", "/tmp2"]);
2865 db.save_workspace(workspace_3.clone()).await;
2866 assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
2867 assert_eq!(
2868 db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
2869 .unwrap(),
2870 workspace_3
2871 );
2872 }
2873
2874 #[gpui::test]
2875 async fn test_session_workspaces() {
2876 zlog::init_test();
2877
2878 let db = WorkspaceDb::open_test_db("test_serializing_workspaces_session_id").await;
2879
2880 let workspace_1 = SerializedWorkspace {
2881 id: WorkspaceId(1),
2882 paths: PathList::new(&["/tmp1"]),
2883 location: SerializedWorkspaceLocation::Local,
2884 center_group: Default::default(),
2885 window_bounds: Default::default(),
2886 display: Default::default(),
2887 docks: Default::default(),
2888 centered_layout: false,
2889 breakpoints: Default::default(),
2890 session_id: Some("session-id-1".to_owned()),
2891 window_id: Some(10),
2892 user_toolchains: Default::default(),
2893 };
2894
2895 let workspace_2 = SerializedWorkspace {
2896 id: WorkspaceId(2),
2897 paths: PathList::new(&["/tmp2"]),
2898 location: SerializedWorkspaceLocation::Local,
2899 center_group: Default::default(),
2900 window_bounds: Default::default(),
2901 display: Default::default(),
2902 docks: Default::default(),
2903 centered_layout: false,
2904 breakpoints: Default::default(),
2905 session_id: Some("session-id-1".to_owned()),
2906 window_id: Some(20),
2907 user_toolchains: Default::default(),
2908 };
2909
2910 let workspace_3 = SerializedWorkspace {
2911 id: WorkspaceId(3),
2912 paths: PathList::new(&["/tmp3"]),
2913 location: SerializedWorkspaceLocation::Local,
2914 center_group: Default::default(),
2915 window_bounds: Default::default(),
2916 display: Default::default(),
2917 docks: Default::default(),
2918 centered_layout: false,
2919 breakpoints: Default::default(),
2920 session_id: Some("session-id-2".to_owned()),
2921 window_id: Some(30),
2922 user_toolchains: Default::default(),
2923 };
2924
2925 let workspace_4 = SerializedWorkspace {
2926 id: WorkspaceId(4),
2927 paths: PathList::new(&["/tmp4"]),
2928 location: SerializedWorkspaceLocation::Local,
2929 center_group: Default::default(),
2930 window_bounds: Default::default(),
2931 display: Default::default(),
2932 docks: Default::default(),
2933 centered_layout: false,
2934 breakpoints: Default::default(),
2935 session_id: None,
2936 window_id: None,
2937 user_toolchains: Default::default(),
2938 };
2939
2940 let connection_id = db
2941 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2942 host: "my-host".into(),
2943 port: Some(1234),
2944 ..Default::default()
2945 }))
2946 .await
2947 .unwrap();
2948
2949 let workspace_5 = SerializedWorkspace {
2950 id: WorkspaceId(5),
2951 paths: PathList::default(),
2952 location: SerializedWorkspaceLocation::Remote(
2953 db.remote_connection(connection_id).unwrap(),
2954 ),
2955 center_group: Default::default(),
2956 window_bounds: Default::default(),
2957 display: Default::default(),
2958 docks: Default::default(),
2959 centered_layout: false,
2960 breakpoints: Default::default(),
2961 session_id: Some("session-id-2".to_owned()),
2962 window_id: Some(50),
2963 user_toolchains: Default::default(),
2964 };
2965
2966 let workspace_6 = SerializedWorkspace {
2967 id: WorkspaceId(6),
2968 paths: PathList::new(&["/tmp6c", "/tmp6b", "/tmp6a"]),
2969 location: SerializedWorkspaceLocation::Local,
2970 center_group: Default::default(),
2971 window_bounds: Default::default(),
2972 breakpoints: Default::default(),
2973 display: Default::default(),
2974 docks: Default::default(),
2975 centered_layout: false,
2976 session_id: Some("session-id-3".to_owned()),
2977 window_id: Some(60),
2978 user_toolchains: Default::default(),
2979 };
2980
2981 db.save_workspace(workspace_1.clone()).await;
2982 thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
2983 db.save_workspace(workspace_2.clone()).await;
2984 db.save_workspace(workspace_3.clone()).await;
2985 thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
2986 db.save_workspace(workspace_4.clone()).await;
2987 db.save_workspace(workspace_5.clone()).await;
2988 db.save_workspace(workspace_6.clone()).await;
2989
2990 let locations = db.session_workspaces("session-id-1".to_owned()).unwrap();
2991 assert_eq!(locations.len(), 2);
2992 assert_eq!(locations[0].0, WorkspaceId(2));
2993 assert_eq!(locations[0].1, PathList::new(&["/tmp2"]));
2994 assert_eq!(locations[0].2, Some(20));
2995 assert_eq!(locations[1].0, WorkspaceId(1));
2996 assert_eq!(locations[1].1, PathList::new(&["/tmp1"]));
2997 assert_eq!(locations[1].2, Some(10));
2998
2999 let locations = db.session_workspaces("session-id-2".to_owned()).unwrap();
3000 assert_eq!(locations.len(), 2);
3001 assert_eq!(locations[0].0, WorkspaceId(5));
3002 assert_eq!(locations[0].1, PathList::default());
3003 assert_eq!(locations[0].2, Some(50));
3004 assert_eq!(locations[0].3, Some(connection_id));
3005 assert_eq!(locations[1].0, WorkspaceId(3));
3006 assert_eq!(locations[1].1, PathList::new(&["/tmp3"]));
3007 assert_eq!(locations[1].2, Some(30));
3008
3009 let locations = db.session_workspaces("session-id-3".to_owned()).unwrap();
3010 assert_eq!(locations.len(), 1);
3011 assert_eq!(locations[0].0, WorkspaceId(6));
3012 assert_eq!(
3013 locations[0].1,
3014 PathList::new(&["/tmp6c", "/tmp6b", "/tmp6a"]),
3015 );
3016 assert_eq!(locations[0].2, Some(60));
3017 }
3018
3019 fn default_workspace<P: AsRef<Path>>(
3020 paths: &[P],
3021 center_group: &SerializedPaneGroup,
3022 ) -> SerializedWorkspace {
3023 SerializedWorkspace {
3024 id: WorkspaceId(4),
3025 paths: PathList::new(paths),
3026 location: SerializedWorkspaceLocation::Local,
3027 center_group: center_group.clone(),
3028 window_bounds: Default::default(),
3029 display: Default::default(),
3030 docks: Default::default(),
3031 breakpoints: Default::default(),
3032 centered_layout: false,
3033 session_id: None,
3034 window_id: None,
3035 user_toolchains: Default::default(),
3036 }
3037 }
3038
3039 #[gpui::test]
3040 async fn test_last_session_workspace_locations() {
3041 let dir1 = tempfile::TempDir::with_prefix("dir1").unwrap();
3042 let dir2 = tempfile::TempDir::with_prefix("dir2").unwrap();
3043 let dir3 = tempfile::TempDir::with_prefix("dir3").unwrap();
3044 let dir4 = tempfile::TempDir::with_prefix("dir4").unwrap();
3045
3046 let db =
3047 WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces").await;
3048
3049 let workspaces = [
3050 (1, vec![dir1.path()], 9),
3051 (2, vec![dir2.path()], 5),
3052 (3, vec![dir3.path()], 8),
3053 (4, vec![dir4.path()], 2),
3054 (5, vec![dir1.path(), dir2.path(), dir3.path()], 3),
3055 (6, vec![dir4.path(), dir3.path(), dir2.path()], 4),
3056 ]
3057 .into_iter()
3058 .map(|(id, paths, window_id)| SerializedWorkspace {
3059 id: WorkspaceId(id),
3060 paths: PathList::new(paths.as_slice()),
3061 location: SerializedWorkspaceLocation::Local,
3062 center_group: Default::default(),
3063 window_bounds: Default::default(),
3064 display: Default::default(),
3065 docks: Default::default(),
3066 centered_layout: false,
3067 session_id: Some("one-session".to_owned()),
3068 breakpoints: Default::default(),
3069 window_id: Some(window_id),
3070 user_toolchains: Default::default(),
3071 })
3072 .collect::<Vec<_>>();
3073
3074 for workspace in workspaces.iter() {
3075 db.save_workspace(workspace.clone()).await;
3076 }
3077
3078 let stack = Some(Vec::from([
3079 WindowId::from(2), // Top
3080 WindowId::from(8),
3081 WindowId::from(5),
3082 WindowId::from(9),
3083 WindowId::from(3),
3084 WindowId::from(4), // Bottom
3085 ]));
3086
3087 let locations = db
3088 .last_session_workspace_locations("one-session", stack)
3089 .unwrap();
3090 assert_eq!(
3091 locations,
3092 [
3093 (
3094 WorkspaceId(4),
3095 SerializedWorkspaceLocation::Local,
3096 PathList::new(&[dir4.path()])
3097 ),
3098 (
3099 WorkspaceId(3),
3100 SerializedWorkspaceLocation::Local,
3101 PathList::new(&[dir3.path()])
3102 ),
3103 (
3104 WorkspaceId(2),
3105 SerializedWorkspaceLocation::Local,
3106 PathList::new(&[dir2.path()])
3107 ),
3108 (
3109 WorkspaceId(1),
3110 SerializedWorkspaceLocation::Local,
3111 PathList::new(&[dir1.path()])
3112 ),
3113 (
3114 WorkspaceId(5),
3115 SerializedWorkspaceLocation::Local,
3116 PathList::new(&[dir1.path(), dir2.path(), dir3.path()])
3117 ),
3118 (
3119 WorkspaceId(6),
3120 SerializedWorkspaceLocation::Local,
3121 PathList::new(&[dir4.path(), dir3.path(), dir2.path()])
3122 ),
3123 ]
3124 );
3125 }
3126
3127 #[gpui::test]
3128 async fn test_last_session_workspace_locations_remote() {
3129 let db =
3130 WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces_remote")
3131 .await;
3132
3133 let remote_connections = [
3134 ("host-1", "my-user-1"),
3135 ("host-2", "my-user-2"),
3136 ("host-3", "my-user-3"),
3137 ("host-4", "my-user-4"),
3138 ]
3139 .into_iter()
3140 .map(|(host, user)| async {
3141 let options = RemoteConnectionOptions::Ssh(SshConnectionOptions {
3142 host: host.into(),
3143 username: Some(user.to_string()),
3144 ..Default::default()
3145 });
3146 db.get_or_create_remote_connection(options.clone())
3147 .await
3148 .unwrap();
3149 options
3150 })
3151 .collect::<Vec<_>>();
3152
3153 let remote_connections = futures::future::join_all(remote_connections).await;
3154
3155 let workspaces = [
3156 (1, remote_connections[0].clone(), 9),
3157 (2, remote_connections[1].clone(), 5),
3158 (3, remote_connections[2].clone(), 8),
3159 (4, remote_connections[3].clone(), 2),
3160 ]
3161 .into_iter()
3162 .map(|(id, remote_connection, window_id)| SerializedWorkspace {
3163 id: WorkspaceId(id),
3164 paths: PathList::default(),
3165 location: SerializedWorkspaceLocation::Remote(remote_connection),
3166 center_group: Default::default(),
3167 window_bounds: Default::default(),
3168 display: Default::default(),
3169 docks: Default::default(),
3170 centered_layout: false,
3171 session_id: Some("one-session".to_owned()),
3172 breakpoints: Default::default(),
3173 window_id: Some(window_id),
3174 user_toolchains: Default::default(),
3175 })
3176 .collect::<Vec<_>>();
3177
3178 for workspace in workspaces.iter() {
3179 db.save_workspace(workspace.clone()).await;
3180 }
3181
3182 let stack = Some(Vec::from([
3183 WindowId::from(2), // Top
3184 WindowId::from(8),
3185 WindowId::from(5),
3186 WindowId::from(9), // Bottom
3187 ]));
3188
3189 let have = db
3190 .last_session_workspace_locations("one-session", stack)
3191 .unwrap();
3192 assert_eq!(have.len(), 4);
3193 assert_eq!(
3194 have[0],
3195 (
3196 WorkspaceId(4),
3197 SerializedWorkspaceLocation::Remote(remote_connections[3].clone()),
3198 PathList::default()
3199 )
3200 );
3201 assert_eq!(
3202 have[1],
3203 (
3204 WorkspaceId(3),
3205 SerializedWorkspaceLocation::Remote(remote_connections[2].clone()),
3206 PathList::default()
3207 )
3208 );
3209 assert_eq!(
3210 have[2],
3211 (
3212 WorkspaceId(2),
3213 SerializedWorkspaceLocation::Remote(remote_connections[1].clone()),
3214 PathList::default()
3215 )
3216 );
3217 assert_eq!(
3218 have[3],
3219 (
3220 WorkspaceId(1),
3221 SerializedWorkspaceLocation::Remote(remote_connections[0].clone()),
3222 PathList::default()
3223 )
3224 );
3225 }
3226
3227 #[gpui::test]
3228 async fn test_get_or_create_ssh_project() {
3229 let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project").await;
3230
3231 let host = "example.com".to_string();
3232 let port = Some(22_u16);
3233 let user = Some("user".to_string());
3234
3235 let connection_id = db
3236 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3237 host: host.clone().into(),
3238 port,
3239 username: user.clone(),
3240 ..Default::default()
3241 }))
3242 .await
3243 .unwrap();
3244
3245 // Test that calling the function again with the same parameters returns the same project
3246 let same_connection = db
3247 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3248 host: host.clone().into(),
3249 port,
3250 username: user.clone(),
3251 ..Default::default()
3252 }))
3253 .await
3254 .unwrap();
3255
3256 assert_eq!(connection_id, same_connection);
3257
3258 // Test with different parameters
3259 let host2 = "otherexample.com".to_string();
3260 let port2 = None;
3261 let user2 = Some("otheruser".to_string());
3262
3263 let different_connection = db
3264 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3265 host: host2.clone().into(),
3266 port: port2,
3267 username: user2.clone(),
3268 ..Default::default()
3269 }))
3270 .await
3271 .unwrap();
3272
3273 assert_ne!(connection_id, different_connection);
3274 }
3275
3276 #[gpui::test]
3277 async fn test_get_or_create_ssh_project_with_null_user() {
3278 let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project_with_null_user").await;
3279
3280 let (host, port, user) = ("example.com".to_string(), None, None);
3281
3282 let connection_id = db
3283 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3284 host: host.clone().into(),
3285 port,
3286 username: None,
3287 ..Default::default()
3288 }))
3289 .await
3290 .unwrap();
3291
3292 let same_connection_id = db
3293 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3294 host: host.clone().into(),
3295 port,
3296 username: user.clone(),
3297 ..Default::default()
3298 }))
3299 .await
3300 .unwrap();
3301
3302 assert_eq!(connection_id, same_connection_id);
3303 }
3304
3305 #[gpui::test]
3306 async fn test_get_remote_connections() {
3307 let db = WorkspaceDb::open_test_db("test_get_remote_connections").await;
3308
3309 let connections = [
3310 ("example.com".to_string(), None, None),
3311 (
3312 "anotherexample.com".to_string(),
3313 Some(123_u16),
3314 Some("user2".to_string()),
3315 ),
3316 ("yetanother.com".to_string(), Some(345_u16), None),
3317 ];
3318
3319 let mut ids = Vec::new();
3320 for (host, port, user) in connections.iter() {
3321 ids.push(
3322 db.get_or_create_remote_connection(RemoteConnectionOptions::Ssh(
3323 SshConnectionOptions {
3324 host: host.clone().into(),
3325 port: *port,
3326 username: user.clone(),
3327 ..Default::default()
3328 },
3329 ))
3330 .await
3331 .unwrap(),
3332 );
3333 }
3334
3335 let stored_connections = db.remote_connections().unwrap();
3336 assert_eq!(
3337 stored_connections,
3338 [
3339 (
3340 ids[0],
3341 RemoteConnectionOptions::Ssh(SshConnectionOptions {
3342 host: "example.com".into(),
3343 port: None,
3344 username: None,
3345 ..Default::default()
3346 }),
3347 ),
3348 (
3349 ids[1],
3350 RemoteConnectionOptions::Ssh(SshConnectionOptions {
3351 host: "anotherexample.com".into(),
3352 port: Some(123),
3353 username: Some("user2".into()),
3354 ..Default::default()
3355 }),
3356 ),
3357 (
3358 ids[2],
3359 RemoteConnectionOptions::Ssh(SshConnectionOptions {
3360 host: "yetanother.com".into(),
3361 port: Some(345),
3362 username: None,
3363 ..Default::default()
3364 }),
3365 ),
3366 ]
3367 .into_iter()
3368 .collect::<HashMap<_, _>>(),
3369 );
3370 }
3371
3372 #[gpui::test]
3373 async fn test_simple_split() {
3374 zlog::init_test();
3375
3376 let db = WorkspaceDb::open_test_db("simple_split").await;
3377
3378 // -----------------
3379 // | 1,2 | 5,6 |
3380 // | - - - | |
3381 // | 3,4 | |
3382 // -----------------
3383 let center_pane = group(
3384 Axis::Horizontal,
3385 vec![
3386 group(
3387 Axis::Vertical,
3388 vec![
3389 SerializedPaneGroup::Pane(SerializedPane::new(
3390 vec![
3391 SerializedItem::new("Terminal", 1, false, false),
3392 SerializedItem::new("Terminal", 2, true, false),
3393 ],
3394 false,
3395 0,
3396 )),
3397 SerializedPaneGroup::Pane(SerializedPane::new(
3398 vec![
3399 SerializedItem::new("Terminal", 4, false, false),
3400 SerializedItem::new("Terminal", 3, true, false),
3401 ],
3402 true,
3403 0,
3404 )),
3405 ],
3406 ),
3407 SerializedPaneGroup::Pane(SerializedPane::new(
3408 vec![
3409 SerializedItem::new("Terminal", 5, true, false),
3410 SerializedItem::new("Terminal", 6, false, false),
3411 ],
3412 false,
3413 0,
3414 )),
3415 ],
3416 );
3417
3418 let workspace = default_workspace(&["/tmp"], ¢er_pane);
3419
3420 db.save_workspace(workspace.clone()).await;
3421
3422 let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
3423
3424 assert_eq!(workspace.center_group, new_workspace.center_group);
3425 }
3426
3427 #[gpui::test]
3428 async fn test_cleanup_panes() {
3429 zlog::init_test();
3430
3431 let db = WorkspaceDb::open_test_db("test_cleanup_panes").await;
3432
3433 let center_pane = group(
3434 Axis::Horizontal,
3435 vec![
3436 group(
3437 Axis::Vertical,
3438 vec![
3439 SerializedPaneGroup::Pane(SerializedPane::new(
3440 vec![
3441 SerializedItem::new("Terminal", 1, false, false),
3442 SerializedItem::new("Terminal", 2, true, false),
3443 ],
3444 false,
3445 0,
3446 )),
3447 SerializedPaneGroup::Pane(SerializedPane::new(
3448 vec![
3449 SerializedItem::new("Terminal", 4, false, false),
3450 SerializedItem::new("Terminal", 3, true, false),
3451 ],
3452 true,
3453 0,
3454 )),
3455 ],
3456 ),
3457 SerializedPaneGroup::Pane(SerializedPane::new(
3458 vec![
3459 SerializedItem::new("Terminal", 5, false, false),
3460 SerializedItem::new("Terminal", 6, true, false),
3461 ],
3462 false,
3463 0,
3464 )),
3465 ],
3466 );
3467
3468 let id = &["/tmp"];
3469
3470 let mut workspace = default_workspace(id, ¢er_pane);
3471
3472 db.save_workspace(workspace.clone()).await;
3473
3474 workspace.center_group = group(
3475 Axis::Vertical,
3476 vec![
3477 SerializedPaneGroup::Pane(SerializedPane::new(
3478 vec![
3479 SerializedItem::new("Terminal", 1, false, false),
3480 SerializedItem::new("Terminal", 2, true, false),
3481 ],
3482 false,
3483 0,
3484 )),
3485 SerializedPaneGroup::Pane(SerializedPane::new(
3486 vec![
3487 SerializedItem::new("Terminal", 4, true, false),
3488 SerializedItem::new("Terminal", 3, false, false),
3489 ],
3490 true,
3491 0,
3492 )),
3493 ],
3494 );
3495
3496 db.save_workspace(workspace.clone()).await;
3497
3498 let new_workspace = db.workspace_for_roots(id).unwrap();
3499
3500 assert_eq!(workspace.center_group, new_workspace.center_group);
3501 }
3502
3503 #[gpui::test]
3504 async fn test_empty_workspace_window_bounds() {
3505 zlog::init_test();
3506
3507 let db = WorkspaceDb::open_test_db("test_empty_workspace_window_bounds").await;
3508 let id = db.next_id().await.unwrap();
3509
3510 // Create a workspace with empty paths (empty workspace)
3511 let empty_paths: &[&str] = &[];
3512 let display_uuid = Uuid::new_v4();
3513 let window_bounds = SerializedWindowBounds(WindowBounds::Windowed(Bounds {
3514 origin: point(px(100.0), px(200.0)),
3515 size: size(px(800.0), px(600.0)),
3516 }));
3517
3518 let workspace = SerializedWorkspace {
3519 id,
3520 paths: PathList::new(empty_paths),
3521 location: SerializedWorkspaceLocation::Local,
3522 center_group: Default::default(),
3523 window_bounds: None,
3524 display: None,
3525 docks: Default::default(),
3526 breakpoints: Default::default(),
3527 centered_layout: false,
3528 session_id: None,
3529 window_id: None,
3530 user_toolchains: Default::default(),
3531 };
3532
3533 // Save the workspace (this creates the record with empty paths)
3534 db.save_workspace(workspace.clone()).await;
3535
3536 // Save window bounds separately (as the actual code does via set_window_open_status)
3537 db.set_window_open_status(id, window_bounds, display_uuid)
3538 .await
3539 .unwrap();
3540
3541 // Empty workspaces cannot be retrieved by paths (they'd all match).
3542 // They must be retrieved by workspace_id.
3543 assert!(db.workspace_for_roots(empty_paths).is_none());
3544
3545 // Retrieve using workspace_for_id instead
3546 let retrieved = db.workspace_for_id(id).unwrap();
3547
3548 // Verify window bounds were persisted
3549 assert_eq!(retrieved.id, id);
3550 assert!(retrieved.window_bounds.is_some());
3551 assert_eq!(retrieved.window_bounds.unwrap().0, window_bounds.0);
3552 assert!(retrieved.display.is_some());
3553 assert_eq!(retrieved.display.unwrap(), display_uuid);
3554 }
3555}