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 chrono::{DateTime, NaiveDateTime, Utc};
12use fs::Fs;
13
14use anyhow::{Context as _, Result, bail};
15use collections::{HashMap, HashSet, IndexSet};
16use db::{
17 kvp::KEY_VALUE_STORE,
18 query,
19 sqlez::{connection::Connection, domain::Domain},
20 sqlez_macros::sql,
21};
22use gpui::{Axis, Bounds, Task, WindowBounds, WindowId, point, size};
23use project::{
24 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
25 trusted_worktrees::{DbTrustedPaths, RemoteHostLocation},
26};
27
28use language::{LanguageName, Toolchain, ToolchainScope};
29use remote::{
30 DockerConnectionOptions, RemoteConnectionOptions, SshConnectionOptions, WslConnectionOptions,
31};
32use serde::{Deserialize, Serialize};
33use sqlez::{
34 bindable::{Bind, Column, StaticColumnCount},
35 statement::Statement,
36 thread_safe_connection::ThreadSafeConnection,
37};
38
39use ui::{App, SharedString, px};
40use util::{ResultExt, maybe, rel_path::RelPath};
41use uuid::Uuid;
42
43use crate::{
44 WorkspaceId,
45 path_list::{PathList, SerializedPathList},
46 persistence::model::RemoteConnectionKind,
47};
48
49use model::{
50 GroupId, ItemId, PaneId, RemoteConnectionId, SerializedItem, SerializedPane,
51 SerializedPaneGroup, SerializedWorkspace,
52};
53
54use self::model::{DockStructure, SerializedWorkspaceLocation, SessionWorkspace};
55
56// https://www.sqlite.org/limits.html
57// > <..> the maximum value of a host parameter number is SQLITE_MAX_VARIABLE_NUMBER,
58// > which defaults to <..> 32766 for SQLite versions after 3.32.0.
59const MAX_QUERY_PLACEHOLDERS: usize = 32000;
60
61fn parse_timestamp(text: &str) -> DateTime<Utc> {
62 NaiveDateTime::parse_from_str(text, "%Y-%m-%d %H:%M:%S")
63 .map(|naive| naive.and_utc())
64 .unwrap_or_else(|_| Utc::now())
65}
66
67#[derive(Copy, Clone, Debug, PartialEq)]
68pub(crate) struct SerializedAxis(pub(crate) gpui::Axis);
69impl sqlez::bindable::StaticColumnCount for SerializedAxis {}
70impl sqlez::bindable::Bind for SerializedAxis {
71 fn bind(
72 &self,
73 statement: &sqlez::statement::Statement,
74 start_index: i32,
75 ) -> anyhow::Result<i32> {
76 match self.0 {
77 gpui::Axis::Horizontal => "Horizontal",
78 gpui::Axis::Vertical => "Vertical",
79 }
80 .bind(statement, start_index)
81 }
82}
83
84impl sqlez::bindable::Column for SerializedAxis {
85 fn column(
86 statement: &mut sqlez::statement::Statement,
87 start_index: i32,
88 ) -> anyhow::Result<(Self, i32)> {
89 String::column(statement, start_index).and_then(|(axis_text, next_index)| {
90 Ok((
91 match axis_text.as_str() {
92 "Horizontal" => Self(Axis::Horizontal),
93 "Vertical" => Self(Axis::Vertical),
94 _ => anyhow::bail!("Stored serialized item kind is incorrect"),
95 },
96 next_index,
97 ))
98 })
99 }
100}
101
102#[derive(Copy, Clone, Debug, PartialEq, Default)]
103pub(crate) struct SerializedWindowBounds(pub(crate) WindowBounds);
104
105impl StaticColumnCount for SerializedWindowBounds {
106 fn column_count() -> usize {
107 5
108 }
109}
110
111impl Bind for SerializedWindowBounds {
112 fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
113 match self.0 {
114 WindowBounds::Windowed(bounds) => {
115 let next_index = statement.bind(&"Windowed", start_index)?;
116 statement.bind(
117 &(
118 SerializedPixels(bounds.origin.x),
119 SerializedPixels(bounds.origin.y),
120 SerializedPixels(bounds.size.width),
121 SerializedPixels(bounds.size.height),
122 ),
123 next_index,
124 )
125 }
126 WindowBounds::Maximized(bounds) => {
127 let next_index = statement.bind(&"Maximized", start_index)?;
128 statement.bind(
129 &(
130 SerializedPixels(bounds.origin.x),
131 SerializedPixels(bounds.origin.y),
132 SerializedPixels(bounds.size.width),
133 SerializedPixels(bounds.size.height),
134 ),
135 next_index,
136 )
137 }
138 WindowBounds::Fullscreen(bounds) => {
139 let next_index = statement.bind(&"FullScreen", start_index)?;
140 statement.bind(
141 &(
142 SerializedPixels(bounds.origin.x),
143 SerializedPixels(bounds.origin.y),
144 SerializedPixels(bounds.size.width),
145 SerializedPixels(bounds.size.height),
146 ),
147 next_index,
148 )
149 }
150 }
151 }
152}
153
154impl Column for SerializedWindowBounds {
155 fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
156 let (window_state, next_index) = String::column(statement, start_index)?;
157 let ((x, y, width, height), _): ((i32, i32, i32, i32), _) =
158 Column::column(statement, next_index)?;
159 let bounds = Bounds {
160 origin: point(px(x as f32), px(y as f32)),
161 size: size(px(width as f32), px(height as f32)),
162 };
163
164 let status = match window_state.as_str() {
165 "Windowed" | "Fixed" => SerializedWindowBounds(WindowBounds::Windowed(bounds)),
166 "Maximized" => SerializedWindowBounds(WindowBounds::Maximized(bounds)),
167 "FullScreen" => SerializedWindowBounds(WindowBounds::Fullscreen(bounds)),
168 _ => bail!("Window State did not have a valid string"),
169 };
170
171 Ok((status, next_index + 4))
172 }
173}
174
175const DEFAULT_WINDOW_BOUNDS_KEY: &str = "default_window_bounds";
176
177pub fn read_default_window_bounds() -> Option<(Uuid, WindowBounds)> {
178 let json_str = KEY_VALUE_STORE
179 .read_kvp(DEFAULT_WINDOW_BOUNDS_KEY)
180 .log_err()
181 .flatten()?;
182
183 let (display_uuid, persisted) =
184 serde_json::from_str::<(Uuid, WindowBoundsJson)>(&json_str).ok()?;
185 Some((display_uuid, persisted.into()))
186}
187
188pub async fn write_default_window_bounds(
189 bounds: WindowBounds,
190 display_uuid: Uuid,
191) -> anyhow::Result<()> {
192 let persisted = WindowBoundsJson::from(bounds);
193 let json_str = serde_json::to_string(&(display_uuid, persisted))?;
194 KEY_VALUE_STORE
195 .write_kvp(DEFAULT_WINDOW_BOUNDS_KEY.to_string(), json_str)
196 .await?;
197 Ok(())
198}
199
200#[derive(Serialize, Deserialize)]
201pub enum WindowBoundsJson {
202 Windowed {
203 x: i32,
204 y: i32,
205 width: i32,
206 height: i32,
207 },
208 Maximized {
209 x: i32,
210 y: i32,
211 width: i32,
212 height: i32,
213 },
214 Fullscreen {
215 x: i32,
216 y: i32,
217 width: i32,
218 height: i32,
219 },
220}
221
222impl From<WindowBounds> for WindowBoundsJson {
223 fn from(b: WindowBounds) -> Self {
224 match b {
225 WindowBounds::Windowed(bounds) => {
226 let origin = bounds.origin;
227 let size = bounds.size;
228 WindowBoundsJson::Windowed {
229 x: f32::from(origin.x).round() as i32,
230 y: f32::from(origin.y).round() as i32,
231 width: f32::from(size.width).round() as i32,
232 height: f32::from(size.height).round() as i32,
233 }
234 }
235 WindowBounds::Maximized(bounds) => {
236 let origin = bounds.origin;
237 let size = bounds.size;
238 WindowBoundsJson::Maximized {
239 x: f32::from(origin.x).round() as i32,
240 y: f32::from(origin.y).round() as i32,
241 width: f32::from(size.width).round() as i32,
242 height: f32::from(size.height).round() as i32,
243 }
244 }
245 WindowBounds::Fullscreen(bounds) => {
246 let origin = bounds.origin;
247 let size = bounds.size;
248 WindowBoundsJson::Fullscreen {
249 x: f32::from(origin.x).round() as i32,
250 y: f32::from(origin.y).round() as i32,
251 width: f32::from(size.width).round() as i32,
252 height: f32::from(size.height).round() as i32,
253 }
254 }
255 }
256 }
257}
258
259impl From<WindowBoundsJson> for WindowBounds {
260 fn from(n: WindowBoundsJson) -> Self {
261 match n {
262 WindowBoundsJson::Windowed {
263 x,
264 y,
265 width,
266 height,
267 } => WindowBounds::Windowed(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::Maximized {
272 x,
273 y,
274 width,
275 height,
276 } => WindowBounds::Maximized(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 WindowBoundsJson::Fullscreen {
281 x,
282 y,
283 width,
284 height,
285 } => WindowBounds::Fullscreen(Bounds {
286 origin: point(px(x as f32), px(y as f32)),
287 size: size(px(width as f32), px(height as f32)),
288 }),
289 }
290 }
291}
292
293fn multi_workspace_states() -> db::kvp::ScopedKeyValueStore<'static> {
294 KEY_VALUE_STORE.scoped("multi_workspace_state")
295}
296
297fn read_multi_workspace_state(window_id: WindowId) -> model::MultiWorkspaceState {
298 multi_workspace_states()
299 .read(&window_id.as_u64().to_string())
300 .log_err()
301 .flatten()
302 .and_then(|json| serde_json::from_str(&json).ok())
303 .unwrap_or_default()
304}
305
306pub async fn write_multi_workspace_state(window_id: WindowId, state: model::MultiWorkspaceState) {
307 if let Ok(json_str) = serde_json::to_string(&state) {
308 multi_workspace_states()
309 .write(window_id.as_u64().to_string(), json_str)
310 .await
311 .log_err();
312 }
313}
314
315pub fn read_serialized_multi_workspaces(
316 session_workspaces: Vec<model::SessionWorkspace>,
317) -> Vec<model::SerializedMultiWorkspace> {
318 let mut window_groups: Vec<Vec<model::SessionWorkspace>> = Vec::new();
319 let mut window_id_to_group: HashMap<WindowId, usize> = HashMap::default();
320
321 for session_workspace in session_workspaces {
322 match session_workspace.window_id {
323 Some(window_id) => {
324 let group_index = *window_id_to_group.entry(window_id).or_insert_with(|| {
325 window_groups.push(Vec::new());
326 window_groups.len() - 1
327 });
328 window_groups[group_index].push(session_workspace);
329 }
330 None => {
331 window_groups.push(vec![session_workspace]);
332 }
333 }
334 }
335
336 window_groups
337 .into_iter()
338 .map(|group| {
339 let window_id = group.first().and_then(|sw| sw.window_id);
340 let state = window_id
341 .map(read_multi_workspace_state)
342 .unwrap_or_default();
343 model::SerializedMultiWorkspace {
344 workspaces: group,
345 state,
346 }
347 })
348 .collect()
349}
350
351const DEFAULT_DOCK_STATE_KEY: &str = "default_dock_state";
352
353pub fn read_default_dock_state() -> Option<DockStructure> {
354 let json_str = KEY_VALUE_STORE
355 .read_kvp(DEFAULT_DOCK_STATE_KEY)
356 .log_err()
357 .flatten()?;
358
359 serde_json::from_str::<DockStructure>(&json_str).ok()
360}
361
362pub async fn write_default_dock_state(docks: DockStructure) -> anyhow::Result<()> {
363 let json_str = serde_json::to_string(&docks)?;
364 KEY_VALUE_STORE
365 .write_kvp(DEFAULT_DOCK_STATE_KEY.to_string(), json_str)
366 .await?;
367 Ok(())
368}
369
370#[derive(Debug)]
371pub struct Breakpoint {
372 pub position: u32,
373 pub message: Option<Arc<str>>,
374 pub condition: Option<Arc<str>>,
375 pub hit_condition: Option<Arc<str>>,
376 pub state: BreakpointState,
377}
378
379/// Wrapper for DB type of a breakpoint
380struct BreakpointStateWrapper<'a>(Cow<'a, BreakpointState>);
381
382impl From<BreakpointState> for BreakpointStateWrapper<'static> {
383 fn from(kind: BreakpointState) -> Self {
384 BreakpointStateWrapper(Cow::Owned(kind))
385 }
386}
387
388impl StaticColumnCount for BreakpointStateWrapper<'_> {
389 fn column_count() -> usize {
390 1
391 }
392}
393
394impl Bind for BreakpointStateWrapper<'_> {
395 fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result<i32> {
396 statement.bind(&self.0.to_int(), start_index)
397 }
398}
399
400impl Column for BreakpointStateWrapper<'_> {
401 fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> {
402 let state = statement.column_int(start_index)?;
403
404 match state {
405 0 => Ok((BreakpointState::Enabled.into(), start_index + 1)),
406 1 => Ok((BreakpointState::Disabled.into(), start_index + 1)),
407 _ => anyhow::bail!("Invalid BreakpointState discriminant {state}"),
408 }
409 }
410}
411
412impl sqlez::bindable::StaticColumnCount for Breakpoint {
413 fn column_count() -> usize {
414 // Position, log message, condition message, and hit condition message
415 4 + BreakpointStateWrapper::column_count()
416 }
417}
418
419impl sqlez::bindable::Bind for Breakpoint {
420 fn bind(
421 &self,
422 statement: &sqlez::statement::Statement,
423 start_index: i32,
424 ) -> anyhow::Result<i32> {
425 let next_index = statement.bind(&self.position, start_index)?;
426 let next_index = statement.bind(&self.message, next_index)?;
427 let next_index = statement.bind(&self.condition, next_index)?;
428 let next_index = statement.bind(&self.hit_condition, next_index)?;
429 statement.bind(
430 &BreakpointStateWrapper(Cow::Borrowed(&self.state)),
431 next_index,
432 )
433 }
434}
435
436impl Column for Breakpoint {
437 fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
438 let position = statement
439 .column_int(start_index)
440 .with_context(|| format!("Failed to read BreakPoint at index {start_index}"))?
441 as u32;
442 let (message, next_index) = Option::<String>::column(statement, start_index + 1)?;
443 let (condition, next_index) = Option::<String>::column(statement, next_index)?;
444 let (hit_condition, next_index) = Option::<String>::column(statement, next_index)?;
445 let (state, next_index) = BreakpointStateWrapper::column(statement, next_index)?;
446
447 Ok((
448 Breakpoint {
449 position,
450 message: message.map(Arc::from),
451 condition: condition.map(Arc::from),
452 hit_condition: hit_condition.map(Arc::from),
453 state: state.0.into_owned(),
454 },
455 next_index,
456 ))
457 }
458}
459
460#[derive(Clone, Debug, PartialEq)]
461struct SerializedPixels(gpui::Pixels);
462impl sqlez::bindable::StaticColumnCount for SerializedPixels {}
463
464impl sqlez::bindable::Bind for SerializedPixels {
465 fn bind(
466 &self,
467 statement: &sqlez::statement::Statement,
468 start_index: i32,
469 ) -> anyhow::Result<i32> {
470 let this: i32 = u32::from(self.0) as _;
471 this.bind(statement, start_index)
472 }
473}
474
475pub struct WorkspaceDb(ThreadSafeConnection);
476
477impl Domain for WorkspaceDb {
478 const NAME: &str = stringify!(WorkspaceDb);
479
480 const MIGRATIONS: &[&str] = &[
481 sql!(
482 CREATE TABLE workspaces(
483 workspace_id INTEGER PRIMARY KEY,
484 workspace_location BLOB UNIQUE,
485 dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
486 dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
487 dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed.
488 left_sidebar_open INTEGER, // Boolean
489 timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
490 FOREIGN KEY(dock_pane) REFERENCES panes(pane_id)
491 ) STRICT;
492
493 CREATE TABLE pane_groups(
494 group_id INTEGER PRIMARY KEY,
495 workspace_id INTEGER NOT NULL,
496 parent_group_id INTEGER, // NULL indicates that this is a root node
497 position INTEGER, // NULL indicates that this is a root node
498 axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal'
499 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
500 ON DELETE CASCADE
501 ON UPDATE CASCADE,
502 FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
503 ) STRICT;
504
505 CREATE TABLE panes(
506 pane_id INTEGER PRIMARY KEY,
507 workspace_id INTEGER NOT NULL,
508 active INTEGER NOT NULL, // Boolean
509 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
510 ON DELETE CASCADE
511 ON UPDATE CASCADE
512 ) STRICT;
513
514 CREATE TABLE center_panes(
515 pane_id INTEGER PRIMARY KEY,
516 parent_group_id INTEGER, // NULL means that this is a root pane
517 position INTEGER, // NULL means that this is a root pane
518 FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
519 ON DELETE CASCADE,
520 FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
521 ) STRICT;
522
523 CREATE TABLE items(
524 item_id INTEGER NOT NULL, // This is the item's view id, so this is not unique
525 workspace_id INTEGER NOT NULL,
526 pane_id INTEGER NOT NULL,
527 kind TEXT NOT NULL,
528 position INTEGER NOT NULL,
529 active INTEGER NOT NULL,
530 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
531 ON DELETE CASCADE
532 ON UPDATE CASCADE,
533 FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
534 ON DELETE CASCADE,
535 PRIMARY KEY(item_id, workspace_id)
536 ) STRICT;
537 ),
538 sql!(
539 ALTER TABLE workspaces ADD COLUMN window_state TEXT;
540 ALTER TABLE workspaces ADD COLUMN window_x REAL;
541 ALTER TABLE workspaces ADD COLUMN window_y REAL;
542 ALTER TABLE workspaces ADD COLUMN window_width REAL;
543 ALTER TABLE workspaces ADD COLUMN window_height REAL;
544 ALTER TABLE workspaces ADD COLUMN display BLOB;
545 ),
546 // Drop foreign key constraint from workspaces.dock_pane to panes table.
547 sql!(
548 CREATE TABLE workspaces_2(
549 workspace_id INTEGER PRIMARY KEY,
550 workspace_location BLOB UNIQUE,
551 dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed.
552 dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed.
553 dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed.
554 left_sidebar_open INTEGER, // Boolean
555 timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
556 window_state TEXT,
557 window_x REAL,
558 window_y REAL,
559 window_width REAL,
560 window_height REAL,
561 display BLOB
562 ) STRICT;
563 INSERT INTO workspaces_2 SELECT * FROM workspaces;
564 DROP TABLE workspaces;
565 ALTER TABLE workspaces_2 RENAME TO workspaces;
566 ),
567 // Add panels related information
568 sql!(
569 ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool
570 ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT;
571 ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool
572 ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT;
573 ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool
574 ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT;
575 ),
576 // Add panel zoom persistence
577 sql!(
578 ALTER TABLE workspaces ADD COLUMN left_dock_zoom INTEGER; //bool
579 ALTER TABLE workspaces ADD COLUMN right_dock_zoom INTEGER; //bool
580 ALTER TABLE workspaces ADD COLUMN bottom_dock_zoom INTEGER; //bool
581 ),
582 // Add pane group flex data
583 sql!(
584 ALTER TABLE pane_groups ADD COLUMN flexes TEXT;
585 ),
586 // Add fullscreen field to workspace
587 // Deprecated, `WindowBounds` holds the fullscreen state now.
588 // Preserving so users can downgrade Zed.
589 sql!(
590 ALTER TABLE workspaces ADD COLUMN fullscreen INTEGER; //bool
591 ),
592 // Add preview field to items
593 sql!(
594 ALTER TABLE items ADD COLUMN preview INTEGER; //bool
595 ),
596 // Add centered_layout field to workspace
597 sql!(
598 ALTER TABLE workspaces ADD COLUMN centered_layout INTEGER; //bool
599 ),
600 sql!(
601 CREATE TABLE remote_projects (
602 remote_project_id INTEGER NOT NULL UNIQUE,
603 path TEXT,
604 dev_server_name TEXT
605 );
606 ALTER TABLE workspaces ADD COLUMN remote_project_id INTEGER;
607 ALTER TABLE workspaces RENAME COLUMN workspace_location TO local_paths;
608 ),
609 sql!(
610 DROP TABLE remote_projects;
611 CREATE TABLE dev_server_projects (
612 id INTEGER NOT NULL UNIQUE,
613 path TEXT,
614 dev_server_name TEXT
615 );
616 ALTER TABLE workspaces DROP COLUMN remote_project_id;
617 ALTER TABLE workspaces ADD COLUMN dev_server_project_id INTEGER;
618 ),
619 sql!(
620 ALTER TABLE workspaces ADD COLUMN local_paths_order BLOB;
621 ),
622 sql!(
623 ALTER TABLE workspaces ADD COLUMN session_id TEXT DEFAULT NULL;
624 ),
625 sql!(
626 ALTER TABLE workspaces ADD COLUMN window_id INTEGER DEFAULT NULL;
627 ),
628 sql!(
629 ALTER TABLE panes ADD COLUMN pinned_count INTEGER DEFAULT 0;
630 ),
631 sql!(
632 CREATE TABLE ssh_projects (
633 id INTEGER PRIMARY KEY,
634 host TEXT NOT NULL,
635 port INTEGER,
636 path TEXT NOT NULL,
637 user TEXT
638 );
639 ALTER TABLE workspaces ADD COLUMN ssh_project_id INTEGER REFERENCES ssh_projects(id) ON DELETE CASCADE;
640 ),
641 sql!(
642 ALTER TABLE ssh_projects RENAME COLUMN path TO paths;
643 ),
644 sql!(
645 CREATE TABLE toolchains (
646 workspace_id INTEGER,
647 worktree_id INTEGER,
648 language_name TEXT NOT NULL,
649 name TEXT NOT NULL,
650 path TEXT NOT NULL,
651 PRIMARY KEY (workspace_id, worktree_id, language_name)
652 );
653 ),
654 sql!(
655 ALTER TABLE toolchains ADD COLUMN raw_json TEXT DEFAULT "{}";
656 ),
657 sql!(
658 CREATE TABLE breakpoints (
659 workspace_id INTEGER NOT NULL,
660 path TEXT NOT NULL,
661 breakpoint_location INTEGER NOT NULL,
662 kind INTEGER NOT NULL,
663 log_message TEXT,
664 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
665 ON DELETE CASCADE
666 ON UPDATE CASCADE
667 );
668 ),
669 sql!(
670 ALTER TABLE workspaces ADD COLUMN local_paths_array TEXT;
671 CREATE UNIQUE INDEX local_paths_array_uq ON workspaces(local_paths_array);
672 ALTER TABLE workspaces ADD COLUMN local_paths_order_array TEXT;
673 ),
674 sql!(
675 ALTER TABLE breakpoints ADD COLUMN state INTEGER DEFAULT(0) NOT NULL
676 ),
677 sql!(
678 ALTER TABLE breakpoints DROP COLUMN kind
679 ),
680 sql!(ALTER TABLE toolchains ADD COLUMN relative_worktree_path TEXT DEFAULT "" NOT NULL),
681 sql!(
682 ALTER TABLE breakpoints ADD COLUMN condition TEXT;
683 ALTER TABLE breakpoints ADD COLUMN hit_condition TEXT;
684 ),
685 sql!(CREATE TABLE toolchains2 (
686 workspace_id INTEGER,
687 worktree_id INTEGER,
688 language_name TEXT NOT NULL,
689 name TEXT NOT NULL,
690 path TEXT NOT NULL,
691 raw_json TEXT NOT NULL,
692 relative_worktree_path TEXT NOT NULL,
693 PRIMARY KEY (workspace_id, worktree_id, language_name, relative_worktree_path)) STRICT;
694 INSERT INTO toolchains2
695 SELECT * FROM toolchains;
696 DROP TABLE toolchains;
697 ALTER TABLE toolchains2 RENAME TO toolchains;
698 ),
699 sql!(
700 CREATE TABLE ssh_connections (
701 id INTEGER PRIMARY KEY,
702 host TEXT NOT NULL,
703 port INTEGER,
704 user TEXT
705 );
706
707 INSERT INTO ssh_connections (host, port, user)
708 SELECT DISTINCT host, port, user
709 FROM ssh_projects;
710
711 CREATE TABLE workspaces_2(
712 workspace_id INTEGER PRIMARY KEY,
713 paths TEXT,
714 paths_order TEXT,
715 ssh_connection_id INTEGER REFERENCES ssh_connections(id),
716 timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
717 window_state TEXT,
718 window_x REAL,
719 window_y REAL,
720 window_width REAL,
721 window_height REAL,
722 display BLOB,
723 left_dock_visible INTEGER,
724 left_dock_active_panel TEXT,
725 right_dock_visible INTEGER,
726 right_dock_active_panel TEXT,
727 bottom_dock_visible INTEGER,
728 bottom_dock_active_panel TEXT,
729 left_dock_zoom INTEGER,
730 right_dock_zoom INTEGER,
731 bottom_dock_zoom INTEGER,
732 fullscreen INTEGER,
733 centered_layout INTEGER,
734 session_id TEXT,
735 window_id INTEGER
736 ) STRICT;
737
738 INSERT
739 INTO workspaces_2
740 SELECT
741 workspaces.workspace_id,
742 CASE
743 WHEN ssh_projects.id IS NOT NULL THEN ssh_projects.paths
744 ELSE
745 CASE
746 WHEN workspaces.local_paths_array IS NULL OR workspaces.local_paths_array = "" THEN
747 NULL
748 ELSE
749 replace(workspaces.local_paths_array, ',', CHAR(10))
750 END
751 END as paths,
752
753 CASE
754 WHEN ssh_projects.id IS NOT NULL THEN ""
755 ELSE workspaces.local_paths_order_array
756 END as paths_order,
757
758 CASE
759 WHEN ssh_projects.id IS NOT NULL THEN (
760 SELECT ssh_connections.id
761 FROM ssh_connections
762 WHERE
763 ssh_connections.host IS ssh_projects.host AND
764 ssh_connections.port IS ssh_projects.port AND
765 ssh_connections.user IS ssh_projects.user
766 )
767 ELSE NULL
768 END as ssh_connection_id,
769
770 workspaces.timestamp,
771 workspaces.window_state,
772 workspaces.window_x,
773 workspaces.window_y,
774 workspaces.window_width,
775 workspaces.window_height,
776 workspaces.display,
777 workspaces.left_dock_visible,
778 workspaces.left_dock_active_panel,
779 workspaces.right_dock_visible,
780 workspaces.right_dock_active_panel,
781 workspaces.bottom_dock_visible,
782 workspaces.bottom_dock_active_panel,
783 workspaces.left_dock_zoom,
784 workspaces.right_dock_zoom,
785 workspaces.bottom_dock_zoom,
786 workspaces.fullscreen,
787 workspaces.centered_layout,
788 workspaces.session_id,
789 workspaces.window_id
790 FROM
791 workspaces LEFT JOIN
792 ssh_projects ON
793 workspaces.ssh_project_id = ssh_projects.id;
794
795 DELETE FROM workspaces_2
796 WHERE workspace_id NOT IN (
797 SELECT MAX(workspace_id)
798 FROM workspaces_2
799 GROUP BY ssh_connection_id, paths
800 );
801
802 DROP TABLE ssh_projects;
803 DROP TABLE workspaces;
804 ALTER TABLE workspaces_2 RENAME TO workspaces;
805
806 CREATE UNIQUE INDEX ix_workspaces_location ON workspaces(ssh_connection_id, paths);
807 ),
808 // Fix any data from when workspaces.paths were briefly encoded as JSON arrays
809 sql!(
810 UPDATE workspaces
811 SET paths = CASE
812 WHEN substr(paths, 1, 2) = '[' || '"' AND substr(paths, -2, 2) = '"' || ']' THEN
813 replace(
814 substr(paths, 3, length(paths) - 4),
815 '"' || ',' || '"',
816 CHAR(10)
817 )
818 ELSE
819 replace(paths, ',', CHAR(10))
820 END
821 WHERE paths IS NOT NULL
822 ),
823 sql!(
824 CREATE TABLE remote_connections(
825 id INTEGER PRIMARY KEY,
826 kind TEXT NOT NULL,
827 host TEXT,
828 port INTEGER,
829 user TEXT,
830 distro TEXT
831 );
832
833 CREATE TABLE workspaces_2(
834 workspace_id INTEGER PRIMARY KEY,
835 paths TEXT,
836 paths_order TEXT,
837 remote_connection_id INTEGER REFERENCES remote_connections(id),
838 timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
839 window_state TEXT,
840 window_x REAL,
841 window_y REAL,
842 window_width REAL,
843 window_height REAL,
844 display BLOB,
845 left_dock_visible INTEGER,
846 left_dock_active_panel TEXT,
847 right_dock_visible INTEGER,
848 right_dock_active_panel TEXT,
849 bottom_dock_visible INTEGER,
850 bottom_dock_active_panel TEXT,
851 left_dock_zoom INTEGER,
852 right_dock_zoom INTEGER,
853 bottom_dock_zoom INTEGER,
854 fullscreen INTEGER,
855 centered_layout INTEGER,
856 session_id TEXT,
857 window_id INTEGER
858 ) STRICT;
859
860 INSERT INTO remote_connections
861 SELECT
862 id,
863 "ssh" as kind,
864 host,
865 port,
866 user,
867 NULL as distro
868 FROM ssh_connections;
869
870 INSERT
871 INTO workspaces_2
872 SELECT
873 workspace_id,
874 paths,
875 paths_order,
876 ssh_connection_id as remote_connection_id,
877 timestamp,
878 window_state,
879 window_x,
880 window_y,
881 window_width,
882 window_height,
883 display,
884 left_dock_visible,
885 left_dock_active_panel,
886 right_dock_visible,
887 right_dock_active_panel,
888 bottom_dock_visible,
889 bottom_dock_active_panel,
890 left_dock_zoom,
891 right_dock_zoom,
892 bottom_dock_zoom,
893 fullscreen,
894 centered_layout,
895 session_id,
896 window_id
897 FROM
898 workspaces;
899
900 DROP TABLE workspaces;
901 ALTER TABLE workspaces_2 RENAME TO workspaces;
902
903 CREATE UNIQUE INDEX ix_workspaces_location ON workspaces(remote_connection_id, paths);
904 ),
905 sql!(CREATE TABLE user_toolchains (
906 remote_connection_id INTEGER,
907 workspace_id INTEGER NOT NULL,
908 worktree_id INTEGER NOT NULL,
909 relative_worktree_path TEXT NOT NULL,
910 language_name TEXT NOT NULL,
911 name TEXT NOT NULL,
912 path TEXT NOT NULL,
913 raw_json TEXT NOT NULL,
914
915 PRIMARY KEY (workspace_id, worktree_id, relative_worktree_path, language_name, name, path, raw_json)
916 ) STRICT;),
917 sql!(
918 DROP TABLE ssh_connections;
919 ),
920 sql!(
921 ALTER TABLE remote_connections ADD COLUMN name TEXT;
922 ALTER TABLE remote_connections ADD COLUMN container_id TEXT;
923 ),
924 sql!(
925 CREATE TABLE IF NOT EXISTS trusted_worktrees (
926 trust_id INTEGER PRIMARY KEY AUTOINCREMENT,
927 absolute_path TEXT,
928 user_name TEXT,
929 host_name TEXT
930 ) STRICT;
931 ),
932 sql!(CREATE TABLE toolchains2 (
933 workspace_id INTEGER,
934 worktree_root_path TEXT NOT NULL,
935 language_name TEXT NOT NULL,
936 name TEXT NOT NULL,
937 path TEXT NOT NULL,
938 raw_json TEXT NOT NULL,
939 relative_worktree_path TEXT NOT NULL,
940 PRIMARY KEY (workspace_id, worktree_root_path, language_name, relative_worktree_path)) STRICT;
941 INSERT OR REPLACE INTO toolchains2
942 // The `instr(paths, '\n') = 0` part allows us to find all
943 // workspaces that have a single worktree, as `\n` is used as a
944 // separator when serializing the workspace paths, so if no `\n` is
945 // found, we know we have a single worktree.
946 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;
947 DROP TABLE toolchains;
948 ALTER TABLE toolchains2 RENAME TO toolchains;
949 ),
950 sql!(CREATE TABLE user_toolchains2 (
951 remote_connection_id INTEGER,
952 workspace_id INTEGER NOT NULL,
953 worktree_root_path TEXT NOT NULL,
954 relative_worktree_path TEXT NOT NULL,
955 language_name TEXT NOT NULL,
956 name TEXT NOT NULL,
957 path TEXT NOT NULL,
958 raw_json TEXT NOT NULL,
959
960 PRIMARY KEY (workspace_id, worktree_root_path, relative_worktree_path, language_name, name, path, raw_json)) STRICT;
961 INSERT OR REPLACE INTO user_toolchains2
962 // The `instr(paths, '\n') = 0` part allows us to find all
963 // workspaces that have a single worktree, as `\n` is used as a
964 // separator when serializing the workspace paths, so if no `\n` is
965 // found, we know we have a single worktree.
966 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;
967 DROP TABLE user_toolchains;
968 ALTER TABLE user_toolchains2 RENAME TO user_toolchains;
969 ),
970 sql!(
971 ALTER TABLE remote_connections ADD COLUMN use_podman BOOLEAN;
972 ),
973 ];
974
975 // Allow recovering from bad migration that was initially shipped to nightly
976 // when introducing the ssh_connections table.
977 fn should_allow_migration_change(_index: usize, old: &str, new: &str) -> bool {
978 old.starts_with("CREATE TABLE ssh_connections")
979 && new.starts_with("CREATE TABLE ssh_connections")
980 }
981}
982
983db::static_connection!(DB, WorkspaceDb, []);
984
985impl WorkspaceDb {
986 /// Returns a serialized workspace for the given worktree_roots. If the passed array
987 /// is empty, the most recent workspace is returned instead. If no workspace for the
988 /// passed roots is stored, returns none.
989 pub(crate) fn workspace_for_roots<P: AsRef<Path>>(
990 &self,
991 worktree_roots: &[P],
992 ) -> Option<SerializedWorkspace> {
993 self.workspace_for_roots_internal(worktree_roots, None)
994 }
995
996 pub(crate) fn remote_workspace_for_roots<P: AsRef<Path>>(
997 &self,
998 worktree_roots: &[P],
999 remote_project_id: RemoteConnectionId,
1000 ) -> Option<SerializedWorkspace> {
1001 self.workspace_for_roots_internal(worktree_roots, Some(remote_project_id))
1002 }
1003
1004 pub(crate) fn workspace_for_roots_internal<P: AsRef<Path>>(
1005 &self,
1006 worktree_roots: &[P],
1007 remote_connection_id: Option<RemoteConnectionId>,
1008 ) -> Option<SerializedWorkspace> {
1009 // paths are sorted before db interactions to ensure that the order of the paths
1010 // doesn't affect the workspace selection for existing workspaces
1011 let root_paths = PathList::new(worktree_roots);
1012
1013 // Empty workspaces cannot be matched by paths (all empty workspaces have paths = "").
1014 // They should only be restored via workspace_for_id during session restoration.
1015 if root_paths.is_empty() && remote_connection_id.is_none() {
1016 return None;
1017 }
1018
1019 // Note that we re-assign the workspace_id here in case it's empty
1020 // and we've grabbed the most recent workspace
1021 let (
1022 workspace_id,
1023 paths,
1024 paths_order,
1025 window_bounds,
1026 display,
1027 centered_layout,
1028 docks,
1029 window_id,
1030 ): (
1031 WorkspaceId,
1032 String,
1033 String,
1034 Option<SerializedWindowBounds>,
1035 Option<Uuid>,
1036 Option<bool>,
1037 DockStructure,
1038 Option<u64>,
1039 ) = self
1040 .select_row_bound(sql! {
1041 SELECT
1042 workspace_id,
1043 paths,
1044 paths_order,
1045 window_state,
1046 window_x,
1047 window_y,
1048 window_width,
1049 window_height,
1050 display,
1051 centered_layout,
1052 left_dock_visible,
1053 left_dock_active_panel,
1054 left_dock_zoom,
1055 right_dock_visible,
1056 right_dock_active_panel,
1057 right_dock_zoom,
1058 bottom_dock_visible,
1059 bottom_dock_active_panel,
1060 bottom_dock_zoom,
1061 window_id
1062 FROM workspaces
1063 WHERE
1064 paths IS ? AND
1065 remote_connection_id IS ?
1066 LIMIT 1
1067 })
1068 .and_then(|mut prepared_statement| {
1069 (prepared_statement)((
1070 root_paths.serialize().paths,
1071 remote_connection_id.map(|id| id.0 as i32),
1072 ))
1073 })
1074 .context("No workspaces found")
1075 .warn_on_err()
1076 .flatten()?;
1077
1078 let paths = PathList::deserialize(&SerializedPathList {
1079 paths,
1080 order: paths_order,
1081 });
1082
1083 let remote_connection_options = if let Some(remote_connection_id) = remote_connection_id {
1084 self.remote_connection(remote_connection_id)
1085 .context("Get remote connection")
1086 .log_err()
1087 } else {
1088 None
1089 };
1090
1091 Some(SerializedWorkspace {
1092 id: workspace_id,
1093 location: match remote_connection_options {
1094 Some(options) => SerializedWorkspaceLocation::Remote(options),
1095 None => SerializedWorkspaceLocation::Local,
1096 },
1097 paths,
1098 center_group: self
1099 .get_center_pane_group(workspace_id)
1100 .context("Getting center group")
1101 .log_err()?,
1102 window_bounds,
1103 centered_layout: centered_layout.unwrap_or(false),
1104 display,
1105 docks,
1106 session_id: None,
1107 breakpoints: self.breakpoints(workspace_id),
1108 window_id,
1109 user_toolchains: self.user_toolchains(workspace_id, remote_connection_id),
1110 })
1111 }
1112
1113 /// Returns the workspace with the given ID, loading all associated data.
1114 pub(crate) fn workspace_for_id(
1115 &self,
1116 workspace_id: WorkspaceId,
1117 ) -> Option<SerializedWorkspace> {
1118 let (
1119 paths,
1120 paths_order,
1121 window_bounds,
1122 display,
1123 centered_layout,
1124 docks,
1125 window_id,
1126 remote_connection_id,
1127 ): (
1128 String,
1129 String,
1130 Option<SerializedWindowBounds>,
1131 Option<Uuid>,
1132 Option<bool>,
1133 DockStructure,
1134 Option<u64>,
1135 Option<i32>,
1136 ) = self
1137 .select_row_bound(sql! {
1138 SELECT
1139 paths,
1140 paths_order,
1141 window_state,
1142 window_x,
1143 window_y,
1144 window_width,
1145 window_height,
1146 display,
1147 centered_layout,
1148 left_dock_visible,
1149 left_dock_active_panel,
1150 left_dock_zoom,
1151 right_dock_visible,
1152 right_dock_active_panel,
1153 right_dock_zoom,
1154 bottom_dock_visible,
1155 bottom_dock_active_panel,
1156 bottom_dock_zoom,
1157 window_id,
1158 remote_connection_id
1159 FROM workspaces
1160 WHERE workspace_id = ?
1161 })
1162 .and_then(|mut prepared_statement| (prepared_statement)(workspace_id))
1163 .context("No workspace found for id")
1164 .warn_on_err()
1165 .flatten()?;
1166
1167 let paths = PathList::deserialize(&SerializedPathList {
1168 paths,
1169 order: paths_order,
1170 });
1171
1172 let remote_connection_id = remote_connection_id.map(|id| RemoteConnectionId(id as u64));
1173 let remote_connection_options = if let Some(remote_connection_id) = remote_connection_id {
1174 self.remote_connection(remote_connection_id)
1175 .context("Get remote connection")
1176 .log_err()
1177 } else {
1178 None
1179 };
1180
1181 Some(SerializedWorkspace {
1182 id: workspace_id,
1183 location: match remote_connection_options {
1184 Some(options) => SerializedWorkspaceLocation::Remote(options),
1185 None => SerializedWorkspaceLocation::Local,
1186 },
1187 paths,
1188 center_group: self
1189 .get_center_pane_group(workspace_id)
1190 .context("Getting center group")
1191 .log_err()?,
1192 window_bounds,
1193 centered_layout: centered_layout.unwrap_or(false),
1194 display,
1195 docks,
1196 session_id: None,
1197 breakpoints: self.breakpoints(workspace_id),
1198 window_id,
1199 user_toolchains: self.user_toolchains(workspace_id, remote_connection_id),
1200 })
1201 }
1202
1203 fn breakpoints(&self, workspace_id: WorkspaceId) -> BTreeMap<Arc<Path>, Vec<SourceBreakpoint>> {
1204 let breakpoints: Result<Vec<(PathBuf, Breakpoint)>> = self
1205 .select_bound(sql! {
1206 SELECT path, breakpoint_location, log_message, condition, hit_condition, state
1207 FROM breakpoints
1208 WHERE workspace_id = ?
1209 })
1210 .and_then(|mut prepared_statement| (prepared_statement)(workspace_id));
1211
1212 match breakpoints {
1213 Ok(bp) => {
1214 if bp.is_empty() {
1215 log::debug!("Breakpoints are empty after querying database for them");
1216 }
1217
1218 let mut map: BTreeMap<Arc<Path>, Vec<SourceBreakpoint>> = Default::default();
1219
1220 for (path, breakpoint) in bp {
1221 let path: Arc<Path> = path.into();
1222 map.entry(path.clone()).or_default().push(SourceBreakpoint {
1223 row: breakpoint.position,
1224 path,
1225 message: breakpoint.message,
1226 condition: breakpoint.condition,
1227 hit_condition: breakpoint.hit_condition,
1228 state: breakpoint.state,
1229 });
1230 }
1231
1232 for (path, bps) in map.iter() {
1233 log::info!(
1234 "Got {} breakpoints from database at path: {}",
1235 bps.len(),
1236 path.to_string_lossy()
1237 );
1238 }
1239
1240 map
1241 }
1242 Err(msg) => {
1243 log::error!("Breakpoints query failed with msg: {msg}");
1244 Default::default()
1245 }
1246 }
1247 }
1248
1249 fn user_toolchains(
1250 &self,
1251 workspace_id: WorkspaceId,
1252 remote_connection_id: Option<RemoteConnectionId>,
1253 ) -> BTreeMap<ToolchainScope, IndexSet<Toolchain>> {
1254 type RowKind = (WorkspaceId, String, String, String, String, String, String);
1255
1256 let toolchains: Vec<RowKind> = self
1257 .select_bound(sql! {
1258 SELECT workspace_id, worktree_root_path, relative_worktree_path,
1259 language_name, name, path, raw_json
1260 FROM user_toolchains WHERE remote_connection_id IS ?1 AND (
1261 workspace_id IN (0, ?2)
1262 )
1263 })
1264 .and_then(|mut statement| {
1265 (statement)((remote_connection_id.map(|id| id.0), workspace_id))
1266 })
1267 .unwrap_or_default();
1268 let mut ret = BTreeMap::<_, IndexSet<_>>::default();
1269
1270 for (
1271 _workspace_id,
1272 worktree_root_path,
1273 relative_worktree_path,
1274 language_name,
1275 name,
1276 path,
1277 raw_json,
1278 ) in toolchains
1279 {
1280 // INTEGER's that are primary keys (like workspace ids, remote connection ids and such) start at 1, so we're safe to
1281 let scope = if _workspace_id == WorkspaceId(0) {
1282 debug_assert_eq!(worktree_root_path, String::default());
1283 debug_assert_eq!(relative_worktree_path, String::default());
1284 ToolchainScope::Global
1285 } else {
1286 debug_assert_eq!(workspace_id, _workspace_id);
1287 debug_assert_eq!(
1288 worktree_root_path == String::default(),
1289 relative_worktree_path == String::default()
1290 );
1291
1292 let Some(relative_path) = RelPath::unix(&relative_worktree_path).log_err() else {
1293 continue;
1294 };
1295 if worktree_root_path != String::default()
1296 && relative_worktree_path != String::default()
1297 {
1298 ToolchainScope::Subproject(
1299 Arc::from(worktree_root_path.as_ref()),
1300 relative_path.into(),
1301 )
1302 } else {
1303 ToolchainScope::Project
1304 }
1305 };
1306 let Ok(as_json) = serde_json::from_str(&raw_json) else {
1307 continue;
1308 };
1309 let toolchain = Toolchain {
1310 name: SharedString::from(name),
1311 path: SharedString::from(path),
1312 language_name: LanguageName::from_proto(language_name),
1313 as_json,
1314 };
1315 ret.entry(scope).or_default().insert(toolchain);
1316 }
1317
1318 ret
1319 }
1320
1321 /// Saves a workspace using the worktree roots. Will garbage collect any workspaces
1322 /// that used this workspace previously
1323 pub(crate) async fn save_workspace(&self, workspace: SerializedWorkspace) {
1324 let paths = workspace.paths.serialize();
1325 log::debug!("Saving workspace at location: {:?}", workspace.location);
1326 self.write(move |conn| {
1327 conn.with_savepoint("update_worktrees", || {
1328 let remote_connection_id = match workspace.location.clone() {
1329 SerializedWorkspaceLocation::Local => None,
1330 SerializedWorkspaceLocation::Remote(connection_options) => {
1331 Some(Self::get_or_create_remote_connection_internal(
1332 conn,
1333 connection_options
1334 )?.0)
1335 }
1336 };
1337
1338 // Clear out panes and pane_groups
1339 conn.exec_bound(sql!(
1340 DELETE FROM pane_groups WHERE workspace_id = ?1;
1341 DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id)
1342 .context("Clearing old panes")?;
1343
1344 conn.exec_bound(
1345 sql!(
1346 DELETE FROM breakpoints WHERE workspace_id = ?1;
1347 )
1348 )?(workspace.id).context("Clearing old breakpoints")?;
1349
1350 for (path, breakpoints) in workspace.breakpoints {
1351 for bp in breakpoints {
1352 let state = BreakpointStateWrapper::from(bp.state);
1353 match conn.exec_bound(sql!(
1354 INSERT INTO breakpoints (workspace_id, path, breakpoint_location, log_message, condition, hit_condition, state)
1355 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7);))?
1356
1357 ((
1358 workspace.id,
1359 path.as_ref(),
1360 bp.row,
1361 bp.message,
1362 bp.condition,
1363 bp.hit_condition,
1364 state,
1365 )) {
1366 Ok(_) => {
1367 log::debug!("Stored breakpoint at row: {} in path: {}", bp.row, path.to_string_lossy())
1368 }
1369 Err(err) => {
1370 log::error!("{err}");
1371 continue;
1372 }
1373 }
1374 }
1375 }
1376
1377 conn.exec_bound(
1378 sql!(
1379 DELETE FROM user_toolchains WHERE workspace_id = ?1;
1380 )
1381 )?(workspace.id).context("Clearing old user toolchains")?;
1382
1383 for (scope, toolchains) in workspace.user_toolchains {
1384 for toolchain in toolchains {
1385 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));
1386 let (workspace_id, worktree_root_path, relative_worktree_path) = match scope {
1387 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())),
1388 ToolchainScope::Project => (Some(workspace.id), None, None),
1389 ToolchainScope::Global => (None, None, None),
1390 };
1391 let args = (remote_connection_id, workspace_id.unwrap_or(WorkspaceId(0)), worktree_root_path.unwrap_or_default(), relative_worktree_path.unwrap_or_default(),
1392 toolchain.language_name.as_ref().to_owned(), toolchain.name.to_string(), toolchain.path.to_string(), toolchain.as_json.to_string());
1393 if let Err(err) = conn.exec_bound(query)?(args) {
1394 log::error!("{err}");
1395 continue;
1396 }
1397 }
1398 }
1399
1400 // Clear out old workspaces with the same paths.
1401 // Skip this for empty workspaces - they are identified by workspace_id, not paths.
1402 // Multiple empty workspaces with different content should coexist.
1403 if !paths.paths.is_empty() {
1404 conn.exec_bound(sql!(
1405 DELETE
1406 FROM workspaces
1407 WHERE
1408 workspace_id != ?1 AND
1409 paths IS ?2 AND
1410 remote_connection_id IS ?3
1411 ))?((
1412 workspace.id,
1413 paths.paths.clone(),
1414 remote_connection_id,
1415 ))
1416 .context("clearing out old locations")?;
1417 }
1418
1419 // Upsert
1420 let query = sql!(
1421 INSERT INTO workspaces(
1422 workspace_id,
1423 paths,
1424 paths_order,
1425 remote_connection_id,
1426 left_dock_visible,
1427 left_dock_active_panel,
1428 left_dock_zoom,
1429 right_dock_visible,
1430 right_dock_active_panel,
1431 right_dock_zoom,
1432 bottom_dock_visible,
1433 bottom_dock_active_panel,
1434 bottom_dock_zoom,
1435 session_id,
1436 window_id,
1437 timestamp
1438 )
1439 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, CURRENT_TIMESTAMP)
1440 ON CONFLICT DO
1441 UPDATE SET
1442 paths = ?2,
1443 paths_order = ?3,
1444 remote_connection_id = ?4,
1445 left_dock_visible = ?5,
1446 left_dock_active_panel = ?6,
1447 left_dock_zoom = ?7,
1448 right_dock_visible = ?8,
1449 right_dock_active_panel = ?9,
1450 right_dock_zoom = ?10,
1451 bottom_dock_visible = ?11,
1452 bottom_dock_active_panel = ?12,
1453 bottom_dock_zoom = ?13,
1454 session_id = ?14,
1455 window_id = ?15,
1456 timestamp = CURRENT_TIMESTAMP
1457 );
1458 let mut prepared_query = conn.exec_bound(query)?;
1459 let args = (
1460 workspace.id,
1461 paths.paths.clone(),
1462 paths.order.clone(),
1463 remote_connection_id,
1464 workspace.docks,
1465 workspace.session_id,
1466 workspace.window_id,
1467 );
1468
1469 prepared_query(args).context("Updating workspace")?;
1470
1471 // Save center pane group
1472 Self::save_pane_group(conn, workspace.id, &workspace.center_group, None)
1473 .context("save pane group in save workspace")?;
1474
1475 Ok(())
1476 })
1477 .log_err();
1478 })
1479 .await;
1480 }
1481
1482 pub(crate) async fn get_or_create_remote_connection(
1483 &self,
1484 options: RemoteConnectionOptions,
1485 ) -> Result<RemoteConnectionId> {
1486 self.write(move |conn| Self::get_or_create_remote_connection_internal(conn, options))
1487 .await
1488 }
1489
1490 fn get_or_create_remote_connection_internal(
1491 this: &Connection,
1492 options: RemoteConnectionOptions,
1493 ) -> Result<RemoteConnectionId> {
1494 let kind;
1495 let user: Option<String>;
1496 let mut host = None;
1497 let mut port = None;
1498 let mut distro = None;
1499 let mut name = None;
1500 let mut container_id = None;
1501 let mut use_podman = None;
1502 match options {
1503 RemoteConnectionOptions::Ssh(options) => {
1504 kind = RemoteConnectionKind::Ssh;
1505 host = Some(options.host.to_string());
1506 port = options.port;
1507 user = options.username;
1508 }
1509 RemoteConnectionOptions::Wsl(options) => {
1510 kind = RemoteConnectionKind::Wsl;
1511 distro = Some(options.distro_name);
1512 user = options.user;
1513 }
1514 RemoteConnectionOptions::Docker(options) => {
1515 kind = RemoteConnectionKind::Docker;
1516 container_id = Some(options.container_id);
1517 name = Some(options.name);
1518 use_podman = Some(options.use_podman);
1519 user = Some(options.remote_user);
1520 }
1521 #[cfg(any(test, feature = "test-support"))]
1522 RemoteConnectionOptions::Mock(options) => {
1523 kind = RemoteConnectionKind::Ssh;
1524 host = Some(format!("mock-{}", options.id));
1525 user = Some(format!("mock-user-{}", options.id));
1526 }
1527 }
1528 Self::get_or_create_remote_connection_query(
1529 this,
1530 kind,
1531 host,
1532 port,
1533 user,
1534 distro,
1535 name,
1536 container_id,
1537 use_podman,
1538 )
1539 }
1540
1541 fn get_or_create_remote_connection_query(
1542 this: &Connection,
1543 kind: RemoteConnectionKind,
1544 host: Option<String>,
1545 port: Option<u16>,
1546 user: Option<String>,
1547 distro: Option<String>,
1548 name: Option<String>,
1549 container_id: Option<String>,
1550 use_podman: Option<bool>,
1551 ) -> Result<RemoteConnectionId> {
1552 if let Some(id) = this.select_row_bound(sql!(
1553 SELECT id
1554 FROM remote_connections
1555 WHERE
1556 kind IS ? AND
1557 host IS ? AND
1558 port IS ? AND
1559 user IS ? AND
1560 distro IS ? AND
1561 name IS ? AND
1562 container_id IS ?
1563 LIMIT 1
1564 ))?((
1565 kind.serialize(),
1566 host.clone(),
1567 port,
1568 user.clone(),
1569 distro.clone(),
1570 name.clone(),
1571 container_id.clone(),
1572 ))? {
1573 Ok(RemoteConnectionId(id))
1574 } else {
1575 let id = this.select_row_bound(sql!(
1576 INSERT INTO remote_connections (
1577 kind,
1578 host,
1579 port,
1580 user,
1581 distro,
1582 name,
1583 container_id,
1584 use_podman
1585 ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)
1586 RETURNING id
1587 ))?((
1588 kind.serialize(),
1589 host,
1590 port,
1591 user,
1592 distro,
1593 name,
1594 container_id,
1595 use_podman,
1596 ))?
1597 .context("failed to insert remote project")?;
1598 Ok(RemoteConnectionId(id))
1599 }
1600 }
1601
1602 query! {
1603 pub async fn next_id() -> Result<WorkspaceId> {
1604 INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id
1605 }
1606 }
1607
1608 fn recent_workspaces(
1609 &self,
1610 ) -> Result<
1611 Vec<(
1612 WorkspaceId,
1613 PathList,
1614 Option<RemoteConnectionId>,
1615 DateTime<Utc>,
1616 )>,
1617 > {
1618 Ok(self
1619 .recent_workspaces_query()?
1620 .into_iter()
1621 .map(|(id, paths, order, remote_connection_id, timestamp)| {
1622 (
1623 id,
1624 PathList::deserialize(&SerializedPathList { paths, order }),
1625 remote_connection_id.map(RemoteConnectionId),
1626 parse_timestamp(×tamp),
1627 )
1628 })
1629 .collect())
1630 }
1631
1632 query! {
1633 fn recent_workspaces_query() -> Result<Vec<(WorkspaceId, String, String, Option<u64>, String)>> {
1634 SELECT workspace_id, paths, paths_order, remote_connection_id, timestamp
1635 FROM workspaces
1636 WHERE
1637 paths IS NOT NULL OR
1638 remote_connection_id IS NOT NULL
1639 ORDER BY timestamp DESC
1640 }
1641 }
1642
1643 fn session_workspaces(
1644 &self,
1645 session_id: String,
1646 ) -> Result<
1647 Vec<(
1648 WorkspaceId,
1649 PathList,
1650 Option<u64>,
1651 Option<RemoteConnectionId>,
1652 )>,
1653 > {
1654 Ok(self
1655 .session_workspaces_query(session_id)?
1656 .into_iter()
1657 .map(
1658 |(workspace_id, paths, order, window_id, remote_connection_id)| {
1659 (
1660 WorkspaceId(workspace_id),
1661 PathList::deserialize(&SerializedPathList { paths, order }),
1662 window_id,
1663 remote_connection_id.map(RemoteConnectionId),
1664 )
1665 },
1666 )
1667 .collect())
1668 }
1669
1670 query! {
1671 fn session_workspaces_query(session_id: String) -> Result<Vec<(i64, String, String, Option<u64>, Option<u64>)>> {
1672 SELECT workspace_id, paths, paths_order, window_id, remote_connection_id
1673 FROM workspaces
1674 WHERE session_id = ?1
1675 ORDER BY timestamp DESC
1676 }
1677 }
1678
1679 query! {
1680 pub fn breakpoints_for_file(workspace_id: WorkspaceId, file_path: &Path) -> Result<Vec<Breakpoint>> {
1681 SELECT breakpoint_location
1682 FROM breakpoints
1683 WHERE workspace_id= ?1 AND path = ?2
1684 }
1685 }
1686
1687 query! {
1688 pub fn clear_breakpoints(file_path: &Path) -> Result<()> {
1689 DELETE FROM breakpoints
1690 WHERE file_path = ?2
1691 }
1692 }
1693
1694 fn remote_connections(&self) -> Result<HashMap<RemoteConnectionId, RemoteConnectionOptions>> {
1695 Ok(self.select(sql!(
1696 SELECT
1697 id, kind, host, port, user, distro, container_id, name, use_podman
1698 FROM
1699 remote_connections
1700 ))?()?
1701 .into_iter()
1702 .filter_map(
1703 |(id, kind, host, port, user, distro, container_id, name, use_podman)| {
1704 Some((
1705 RemoteConnectionId(id),
1706 Self::remote_connection_from_row(
1707 kind,
1708 host,
1709 port,
1710 user,
1711 distro,
1712 container_id,
1713 name,
1714 use_podman,
1715 )?,
1716 ))
1717 },
1718 )
1719 .collect())
1720 }
1721
1722 pub(crate) fn remote_connection(
1723 &self,
1724 id: RemoteConnectionId,
1725 ) -> Result<RemoteConnectionOptions> {
1726 let (kind, host, port, user, distro, container_id, name, use_podman) =
1727 self.select_row_bound(sql!(
1728 SELECT kind, host, port, user, distro, container_id, name, use_podman
1729 FROM remote_connections
1730 WHERE id = ?
1731 ))?(id.0)?
1732 .context("no such remote connection")?;
1733 Self::remote_connection_from_row(
1734 kind,
1735 host,
1736 port,
1737 user,
1738 distro,
1739 container_id,
1740 name,
1741 use_podman,
1742 )
1743 .context("invalid remote_connection row")
1744 }
1745
1746 fn remote_connection_from_row(
1747 kind: String,
1748 host: Option<String>,
1749 port: Option<u16>,
1750 user: Option<String>,
1751 distro: Option<String>,
1752 container_id: Option<String>,
1753 name: Option<String>,
1754 use_podman: Option<bool>,
1755 ) -> Option<RemoteConnectionOptions> {
1756 match RemoteConnectionKind::deserialize(&kind)? {
1757 RemoteConnectionKind::Wsl => Some(RemoteConnectionOptions::Wsl(WslConnectionOptions {
1758 distro_name: distro?,
1759 user: user,
1760 })),
1761 RemoteConnectionKind::Ssh => Some(RemoteConnectionOptions::Ssh(SshConnectionOptions {
1762 host: host?.into(),
1763 port,
1764 username: user,
1765 ..Default::default()
1766 })),
1767 RemoteConnectionKind::Docker => {
1768 Some(RemoteConnectionOptions::Docker(DockerConnectionOptions {
1769 container_id: container_id?,
1770 name: name?,
1771 remote_user: user?,
1772 upload_binary_over_docker_exec: false,
1773 use_podman: use_podman?,
1774 }))
1775 }
1776 }
1777 }
1778
1779 query! {
1780 pub async fn delete_workspace_by_id(id: WorkspaceId) -> Result<()> {
1781 DELETE FROM workspaces
1782 WHERE workspace_id IS ?
1783 }
1784 }
1785
1786 async fn all_paths_exist_with_a_directory(paths: &[PathBuf], fs: &dyn Fs) -> bool {
1787 let mut any_dir = false;
1788 for path in paths {
1789 match fs.metadata(path).await.ok().flatten() {
1790 None => return false,
1791 Some(meta) => {
1792 if meta.is_dir {
1793 any_dir = true;
1794 }
1795 }
1796 }
1797 }
1798 any_dir
1799 }
1800
1801 // Returns the recent locations which are still valid on disk and deletes ones which no longer
1802 // exist.
1803 pub async fn recent_workspaces_on_disk(
1804 &self,
1805 fs: &dyn Fs,
1806 ) -> Result<
1807 Vec<(
1808 WorkspaceId,
1809 SerializedWorkspaceLocation,
1810 PathList,
1811 DateTime<Utc>,
1812 )>,
1813 > {
1814 let mut result = Vec::new();
1815 let mut delete_tasks = Vec::new();
1816 let remote_connections = self.remote_connections()?;
1817
1818 for (id, paths, remote_connection_id, timestamp) in self.recent_workspaces()? {
1819 if let Some(remote_connection_id) = remote_connection_id {
1820 if let Some(connection_options) = remote_connections.get(&remote_connection_id) {
1821 result.push((
1822 id,
1823 SerializedWorkspaceLocation::Remote(connection_options.clone()),
1824 paths,
1825 timestamp,
1826 ));
1827 } else {
1828 delete_tasks.push(self.delete_workspace_by_id(id));
1829 }
1830 continue;
1831 }
1832
1833 let has_wsl_path = if cfg!(windows) {
1834 paths
1835 .paths()
1836 .iter()
1837 .any(|path| util::paths::WslPath::from_path(path).is_some())
1838 } else {
1839 false
1840 };
1841
1842 // Delete the workspace if any of the paths are WSL paths.
1843 // If a local workspace points to WSL, this check will cause us to wait for the
1844 // WSL VM and file server to boot up. This can block for many seconds.
1845 // Supported scenarios use remote workspaces.
1846 if !has_wsl_path && Self::all_paths_exist_with_a_directory(paths.paths(), fs).await {
1847 result.push((id, SerializedWorkspaceLocation::Local, paths, timestamp));
1848 } else {
1849 delete_tasks.push(self.delete_workspace_by_id(id));
1850 }
1851 }
1852
1853 futures::future::join_all(delete_tasks).await;
1854 Ok(result)
1855 }
1856
1857 pub async fn last_workspace(
1858 &self,
1859 fs: &dyn Fs,
1860 ) -> Result<
1861 Option<(
1862 WorkspaceId,
1863 SerializedWorkspaceLocation,
1864 PathList,
1865 DateTime<Utc>,
1866 )>,
1867 > {
1868 Ok(self.recent_workspaces_on_disk(fs).await?.into_iter().next())
1869 }
1870
1871 // Returns the locations of the workspaces that were still opened when the last
1872 // session was closed (i.e. when Zed was quit).
1873 // If `last_session_window_order` is provided, the returned locations are ordered
1874 // according to that.
1875 pub async fn last_session_workspace_locations(
1876 &self,
1877 last_session_id: &str,
1878 last_session_window_stack: Option<Vec<WindowId>>,
1879 fs: &dyn Fs,
1880 ) -> Result<Vec<SessionWorkspace>> {
1881 let mut workspaces = Vec::new();
1882
1883 for (workspace_id, paths, window_id, remote_connection_id) in
1884 self.session_workspaces(last_session_id.to_owned())?
1885 {
1886 let window_id = window_id.map(WindowId::from);
1887
1888 if let Some(remote_connection_id) = remote_connection_id {
1889 workspaces.push(SessionWorkspace {
1890 workspace_id,
1891 location: SerializedWorkspaceLocation::Remote(
1892 self.remote_connection(remote_connection_id)?,
1893 ),
1894 paths,
1895 window_id,
1896 });
1897 } else if paths.is_empty() {
1898 // Empty workspace with items (drafts, files) - include for restoration
1899 workspaces.push(SessionWorkspace {
1900 workspace_id,
1901 location: SerializedWorkspaceLocation::Local,
1902 paths,
1903 window_id,
1904 });
1905 } else {
1906 if Self::all_paths_exist_with_a_directory(paths.paths(), fs).await {
1907 workspaces.push(SessionWorkspace {
1908 workspace_id,
1909 location: SerializedWorkspaceLocation::Local,
1910 paths,
1911 window_id,
1912 });
1913 }
1914 }
1915 }
1916
1917 if let Some(stack) = last_session_window_stack {
1918 workspaces.sort_by_key(|workspace| {
1919 workspace
1920 .window_id
1921 .and_then(|id| stack.iter().position(|&order_id| order_id == id))
1922 .unwrap_or(usize::MAX)
1923 });
1924 }
1925
1926 Ok(workspaces)
1927 }
1928
1929 fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
1930 Ok(self
1931 .get_pane_group(workspace_id, None)?
1932 .into_iter()
1933 .next()
1934 .unwrap_or_else(|| {
1935 SerializedPaneGroup::Pane(SerializedPane {
1936 active: true,
1937 children: vec![],
1938 pinned_count: 0,
1939 })
1940 }))
1941 }
1942
1943 fn get_pane_group(
1944 &self,
1945 workspace_id: WorkspaceId,
1946 group_id: Option<GroupId>,
1947 ) -> Result<Vec<SerializedPaneGroup>> {
1948 type GroupKey = (Option<GroupId>, WorkspaceId);
1949 type GroupOrPane = (
1950 Option<GroupId>,
1951 Option<SerializedAxis>,
1952 Option<PaneId>,
1953 Option<bool>,
1954 Option<usize>,
1955 Option<String>,
1956 );
1957 self.select_bound::<GroupKey, GroupOrPane>(sql!(
1958 SELECT group_id, axis, pane_id, active, pinned_count, flexes
1959 FROM (SELECT
1960 group_id,
1961 axis,
1962 NULL as pane_id,
1963 NULL as active,
1964 NULL as pinned_count,
1965 position,
1966 parent_group_id,
1967 workspace_id,
1968 flexes
1969 FROM pane_groups
1970 UNION
1971 SELECT
1972 NULL,
1973 NULL,
1974 center_panes.pane_id,
1975 panes.active as active,
1976 pinned_count,
1977 position,
1978 parent_group_id,
1979 panes.workspace_id as workspace_id,
1980 NULL
1981 FROM center_panes
1982 JOIN panes ON center_panes.pane_id = panes.pane_id)
1983 WHERE parent_group_id IS ? AND workspace_id = ?
1984 ORDER BY position
1985 ))?((group_id, workspace_id))?
1986 .into_iter()
1987 .map(|(group_id, axis, pane_id, active, pinned_count, flexes)| {
1988 let maybe_pane = maybe!({ Some((pane_id?, active?, pinned_count?)) });
1989 if let Some((group_id, axis)) = group_id.zip(axis) {
1990 let flexes = flexes
1991 .map(|flexes: String| serde_json::from_str::<Vec<f32>>(&flexes))
1992 .transpose()?;
1993
1994 Ok(SerializedPaneGroup::Group {
1995 axis,
1996 children: self.get_pane_group(workspace_id, Some(group_id))?,
1997 flexes,
1998 })
1999 } else if let Some((pane_id, active, pinned_count)) = maybe_pane {
2000 Ok(SerializedPaneGroup::Pane(SerializedPane::new(
2001 self.get_items(pane_id)?,
2002 active,
2003 pinned_count,
2004 )))
2005 } else {
2006 bail!("Pane Group Child was neither a pane group or a pane");
2007 }
2008 })
2009 // Filter out panes and pane groups which don't have any children or items
2010 .filter(|pane_group| match pane_group {
2011 Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(),
2012 Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(),
2013 _ => true,
2014 })
2015 .collect::<Result<_>>()
2016 }
2017
2018 fn save_pane_group(
2019 conn: &Connection,
2020 workspace_id: WorkspaceId,
2021 pane_group: &SerializedPaneGroup,
2022 parent: Option<(GroupId, usize)>,
2023 ) -> Result<()> {
2024 if parent.is_none() {
2025 log::debug!("Saving a pane group for workspace {workspace_id:?}");
2026 }
2027 match pane_group {
2028 SerializedPaneGroup::Group {
2029 axis,
2030 children,
2031 flexes,
2032 } => {
2033 let (parent_id, position) = parent.unzip();
2034
2035 let flex_string = flexes
2036 .as_ref()
2037 .map(|flexes| serde_json::json!(flexes).to_string());
2038
2039 let group_id = conn.select_row_bound::<_, i64>(sql!(
2040 INSERT INTO pane_groups(
2041 workspace_id,
2042 parent_group_id,
2043 position,
2044 axis,
2045 flexes
2046 )
2047 VALUES (?, ?, ?, ?, ?)
2048 RETURNING group_id
2049 ))?((
2050 workspace_id,
2051 parent_id,
2052 position,
2053 *axis,
2054 flex_string,
2055 ))?
2056 .context("Couldn't retrieve group_id from inserted pane_group")?;
2057
2058 for (position, group) in children.iter().enumerate() {
2059 Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))?
2060 }
2061
2062 Ok(())
2063 }
2064 SerializedPaneGroup::Pane(pane) => {
2065 Self::save_pane(conn, workspace_id, pane, parent)?;
2066 Ok(())
2067 }
2068 }
2069 }
2070
2071 fn save_pane(
2072 conn: &Connection,
2073 workspace_id: WorkspaceId,
2074 pane: &SerializedPane,
2075 parent: Option<(GroupId, usize)>,
2076 ) -> Result<PaneId> {
2077 let pane_id = conn.select_row_bound::<_, i64>(sql!(
2078 INSERT INTO panes(workspace_id, active, pinned_count)
2079 VALUES (?, ?, ?)
2080 RETURNING pane_id
2081 ))?((workspace_id, pane.active, pane.pinned_count))?
2082 .context("Could not retrieve inserted pane_id")?;
2083
2084 let (parent_id, order) = parent.unzip();
2085 conn.exec_bound(sql!(
2086 INSERT INTO center_panes(pane_id, parent_group_id, position)
2087 VALUES (?, ?, ?)
2088 ))?((pane_id, parent_id, order))?;
2089
2090 Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?;
2091
2092 Ok(pane_id)
2093 }
2094
2095 fn get_items(&self, pane_id: PaneId) -> Result<Vec<SerializedItem>> {
2096 self.select_bound(sql!(
2097 SELECT kind, item_id, active, preview FROM items
2098 WHERE pane_id = ?
2099 ORDER BY position
2100 ))?(pane_id)
2101 }
2102
2103 fn save_items(
2104 conn: &Connection,
2105 workspace_id: WorkspaceId,
2106 pane_id: PaneId,
2107 items: &[SerializedItem],
2108 ) -> Result<()> {
2109 let mut insert = conn.exec_bound(sql!(
2110 INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active, preview) VALUES (?, ?, ?, ?, ?, ?, ?)
2111 )).context("Preparing insertion")?;
2112 for (position, item) in items.iter().enumerate() {
2113 insert((workspace_id, pane_id, position, item))?;
2114 }
2115
2116 Ok(())
2117 }
2118
2119 query! {
2120 pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> {
2121 UPDATE workspaces
2122 SET timestamp = CURRENT_TIMESTAMP
2123 WHERE workspace_id = ?
2124 }
2125 }
2126
2127 query! {
2128 pub(crate) async fn set_window_open_status(workspace_id: WorkspaceId, bounds: SerializedWindowBounds, display: Uuid) -> Result<()> {
2129 UPDATE workspaces
2130 SET window_state = ?2,
2131 window_x = ?3,
2132 window_y = ?4,
2133 window_width = ?5,
2134 window_height = ?6,
2135 display = ?7
2136 WHERE workspace_id = ?1
2137 }
2138 }
2139
2140 query! {
2141 pub(crate) async fn set_centered_layout(workspace_id: WorkspaceId, centered_layout: bool) -> Result<()> {
2142 UPDATE workspaces
2143 SET centered_layout = ?2
2144 WHERE workspace_id = ?1
2145 }
2146 }
2147
2148 query! {
2149 pub(crate) async fn set_session_id(workspace_id: WorkspaceId, session_id: Option<String>) -> Result<()> {
2150 UPDATE workspaces
2151 SET session_id = ?2
2152 WHERE workspace_id = ?1
2153 }
2154 }
2155
2156 query! {
2157 pub(crate) async fn set_session_binding(workspace_id: WorkspaceId, session_id: Option<String>, window_id: Option<u64>) -> Result<()> {
2158 UPDATE workspaces
2159 SET session_id = ?2, window_id = ?3
2160 WHERE workspace_id = ?1
2161 }
2162 }
2163
2164 pub(crate) async fn toolchains(
2165 &self,
2166 workspace_id: WorkspaceId,
2167 ) -> Result<Vec<(Toolchain, Arc<Path>, Arc<RelPath>)>> {
2168 self.write(move |this| {
2169 let mut select = this
2170 .select_bound(sql!(
2171 SELECT
2172 name, path, worktree_root_path, relative_worktree_path, language_name, raw_json
2173 FROM toolchains
2174 WHERE workspace_id = ?
2175 ))
2176 .context("select toolchains")?;
2177
2178 let toolchain: Vec<(String, String, String, String, String, String)> =
2179 select(workspace_id)?;
2180
2181 Ok(toolchain
2182 .into_iter()
2183 .filter_map(
2184 |(name, path, worktree_root_path, relative_worktree_path, language, json)| {
2185 Some((
2186 Toolchain {
2187 name: name.into(),
2188 path: path.into(),
2189 language_name: LanguageName::new(&language),
2190 as_json: serde_json::Value::from_str(&json).ok()?,
2191 },
2192 Arc::from(worktree_root_path.as_ref()),
2193 RelPath::from_proto(&relative_worktree_path).log_err()?,
2194 ))
2195 },
2196 )
2197 .collect())
2198 })
2199 .await
2200 }
2201
2202 pub async fn set_toolchain(
2203 &self,
2204 workspace_id: WorkspaceId,
2205 worktree_root_path: Arc<Path>,
2206 relative_worktree_path: Arc<RelPath>,
2207 toolchain: Toolchain,
2208 ) -> Result<()> {
2209 log::debug!(
2210 "Setting toolchain for workspace, worktree: {worktree_root_path:?}, relative path: {relative_worktree_path:?}, toolchain: {}",
2211 toolchain.name
2212 );
2213 self.write(move |conn| {
2214 let mut insert = conn
2215 .exec_bound(sql!(
2216 INSERT INTO toolchains(workspace_id, worktree_root_path, relative_worktree_path, language_name, name, path, raw_json) VALUES (?, ?, ?, ?, ?, ?, ?)
2217 ON CONFLICT DO
2218 UPDATE SET
2219 name = ?5,
2220 path = ?6,
2221 raw_json = ?7
2222 ))
2223 .context("Preparing insertion")?;
2224
2225 insert((
2226 workspace_id,
2227 worktree_root_path.to_string_lossy().into_owned(),
2228 relative_worktree_path.as_unix_str(),
2229 toolchain.language_name.as_ref(),
2230 toolchain.name.as_ref(),
2231 toolchain.path.as_ref(),
2232 toolchain.as_json.to_string(),
2233 ))?;
2234
2235 Ok(())
2236 }).await
2237 }
2238
2239 pub(crate) async fn save_trusted_worktrees(
2240 &self,
2241 trusted_worktrees: HashMap<Option<RemoteHostLocation>, HashSet<PathBuf>>,
2242 ) -> anyhow::Result<()> {
2243 use anyhow::Context as _;
2244 use db::sqlez::statement::Statement;
2245 use itertools::Itertools as _;
2246
2247 DB.clear_trusted_worktrees()
2248 .await
2249 .context("clearing previous trust state")?;
2250
2251 let trusted_worktrees = trusted_worktrees
2252 .into_iter()
2253 .flat_map(|(host, abs_paths)| {
2254 abs_paths
2255 .into_iter()
2256 .map(move |abs_path| (Some(abs_path), host.clone()))
2257 })
2258 .collect::<Vec<_>>();
2259 let mut first_worktree;
2260 let mut last_worktree = 0_usize;
2261 for (count, placeholders) in std::iter::once("(?, ?, ?)")
2262 .cycle()
2263 .take(trusted_worktrees.len())
2264 .chunks(MAX_QUERY_PLACEHOLDERS / 3)
2265 .into_iter()
2266 .map(|chunk| {
2267 let mut count = 0;
2268 let placeholders = chunk
2269 .inspect(|_| {
2270 count += 1;
2271 })
2272 .join(", ");
2273 (count, placeholders)
2274 })
2275 .collect::<Vec<_>>()
2276 {
2277 first_worktree = last_worktree;
2278 last_worktree = last_worktree + count;
2279 let query = format!(
2280 r#"INSERT INTO trusted_worktrees(absolute_path, user_name, host_name)
2281VALUES {placeholders};"#
2282 );
2283
2284 let trusted_worktrees = trusted_worktrees[first_worktree..last_worktree].to_vec();
2285 self.write(move |conn| {
2286 let mut statement = Statement::prepare(conn, query)?;
2287 let mut next_index = 1;
2288 for (abs_path, host) in trusted_worktrees {
2289 let abs_path = abs_path.as_ref().map(|abs_path| abs_path.to_string_lossy());
2290 next_index = statement.bind(
2291 &abs_path.as_ref().map(|abs_path| abs_path.as_ref()),
2292 next_index,
2293 )?;
2294 next_index = statement.bind(
2295 &host
2296 .as_ref()
2297 .and_then(|host| Some(host.user_name.as_ref()?.as_str())),
2298 next_index,
2299 )?;
2300 next_index = statement.bind(
2301 &host.as_ref().map(|host| host.host_identifier.as_str()),
2302 next_index,
2303 )?;
2304 }
2305 statement.exec()
2306 })
2307 .await
2308 .context("inserting new trusted state")?;
2309 }
2310 Ok(())
2311 }
2312
2313 pub fn fetch_trusted_worktrees(&self) -> Result<DbTrustedPaths> {
2314 let trusted_worktrees = DB.trusted_worktrees()?;
2315 Ok(trusted_worktrees
2316 .into_iter()
2317 .filter_map(|(abs_path, user_name, host_name)| {
2318 let db_host = match (user_name, host_name) {
2319 (None, Some(host_name)) => Some(RemoteHostLocation {
2320 user_name: None,
2321 host_identifier: SharedString::new(host_name),
2322 }),
2323 (Some(user_name), Some(host_name)) => Some(RemoteHostLocation {
2324 user_name: Some(SharedString::new(user_name)),
2325 host_identifier: SharedString::new(host_name),
2326 }),
2327 _ => None,
2328 };
2329 Some((db_host, abs_path?))
2330 })
2331 .fold(HashMap::default(), |mut acc, (remote_host, abs_path)| {
2332 acc.entry(remote_host)
2333 .or_insert_with(HashSet::default)
2334 .insert(abs_path);
2335 acc
2336 }))
2337 }
2338
2339 query! {
2340 fn trusted_worktrees() -> Result<Vec<(Option<PathBuf>, Option<String>, Option<String>)>> {
2341 SELECT absolute_path, user_name, host_name
2342 FROM trusted_worktrees
2343 }
2344 }
2345
2346 query! {
2347 pub async fn clear_trusted_worktrees() -> Result<()> {
2348 DELETE FROM trusted_worktrees
2349 }
2350 }
2351}
2352
2353pub fn delete_unloaded_items(
2354 alive_items: Vec<ItemId>,
2355 workspace_id: WorkspaceId,
2356 table: &'static str,
2357 db: &ThreadSafeConnection,
2358 cx: &mut App,
2359) -> Task<Result<()>> {
2360 let db = db.clone();
2361 cx.spawn(async move |_| {
2362 let placeholders = alive_items
2363 .iter()
2364 .map(|_| "?")
2365 .collect::<Vec<&str>>()
2366 .join(", ");
2367
2368 let query = format!(
2369 "DELETE FROM {table} WHERE workspace_id = ? AND item_id NOT IN ({placeholders})"
2370 );
2371
2372 db.write(move |conn| {
2373 let mut statement = Statement::prepare(conn, query)?;
2374 let mut next_index = statement.bind(&workspace_id, 1)?;
2375 for id in alive_items {
2376 next_index = statement.bind(&id, next_index)?;
2377 }
2378 statement.exec()
2379 })
2380 .await
2381 })
2382}
2383
2384#[cfg(test)]
2385mod tests {
2386 use super::*;
2387 use crate::persistence::model::{
2388 SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace, SessionWorkspace,
2389 };
2390 use gpui;
2391 use pretty_assertions::assert_eq;
2392 use remote::SshConnectionOptions;
2393 use serde_json::json;
2394 use std::{thread, time::Duration};
2395
2396 #[gpui::test]
2397 async fn test_multi_workspace_serializes_on_add_and_remove(cx: &mut gpui::TestAppContext) {
2398 use crate::multi_workspace::MultiWorkspace;
2399 use crate::persistence::read_multi_workspace_state;
2400 use feature_flags::FeatureFlagAppExt;
2401 use gpui::AppContext as _;
2402 use project::Project;
2403
2404 crate::tests::init_test(cx);
2405
2406 cx.update(|cx| {
2407 cx.set_staff(true);
2408 cx.update_flags(true, vec!["agent-v2".to_string()]);
2409 });
2410
2411 let fs = fs::FakeFs::new(cx.executor());
2412 let project1 = Project::test(fs.clone(), [], cx).await;
2413 let project2 = Project::test(fs.clone(), [], cx).await;
2414
2415 let (multi_workspace, cx) =
2416 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project1.clone(), window, cx));
2417
2418 multi_workspace.update_in(cx, |mw, _, cx| {
2419 mw.set_random_database_id(cx);
2420 });
2421
2422 let window_id =
2423 multi_workspace.update_in(cx, |_, window, _cx| window.window_handle().window_id());
2424
2425 // --- Add a second workspace ---
2426 let workspace2 = multi_workspace.update_in(cx, |mw, window, cx| {
2427 let workspace = cx.new(|cx| crate::Workspace::test_new(project2.clone(), window, cx));
2428 workspace.update(cx, |ws, _cx| ws.set_random_database_id());
2429 mw.activate(workspace.clone(), cx);
2430 workspace
2431 });
2432
2433 // Run background tasks so serialize has a chance to flush.
2434 cx.run_until_parked();
2435
2436 // Read back the persisted state and check that the active workspace ID was written.
2437 let state_after_add = read_multi_workspace_state(window_id);
2438 let active_workspace2_db_id = workspace2.read_with(cx, |ws, _| ws.database_id());
2439 assert_eq!(
2440 state_after_add.active_workspace_id, active_workspace2_db_id,
2441 "After adding a second workspace, the serialized active_workspace_id should match \
2442 the newly activated workspace's database id"
2443 );
2444
2445 // --- Remove the second workspace (index 1) ---
2446 multi_workspace.update_in(cx, |mw, window, cx| {
2447 mw.remove_workspace(1, window, cx);
2448 });
2449
2450 cx.run_until_parked();
2451
2452 let state_after_remove = read_multi_workspace_state(window_id);
2453 let remaining_db_id =
2454 multi_workspace.read_with(cx, |mw, cx| mw.workspace().read(cx).database_id());
2455 assert_eq!(
2456 state_after_remove.active_workspace_id, remaining_db_id,
2457 "After removing a workspace, the serialized active_workspace_id should match \
2458 the remaining active workspace's database id"
2459 );
2460 }
2461
2462 #[gpui::test]
2463 async fn test_breakpoints() {
2464 zlog::init_test();
2465
2466 let db = WorkspaceDb::open_test_db("test_breakpoints").await;
2467 let id = db.next_id().await.unwrap();
2468
2469 let path = Path::new("/tmp/test.rs");
2470
2471 let breakpoint = Breakpoint {
2472 position: 123,
2473 message: None,
2474 state: BreakpointState::Enabled,
2475 condition: None,
2476 hit_condition: None,
2477 };
2478
2479 let log_breakpoint = Breakpoint {
2480 position: 456,
2481 message: Some("Test log message".into()),
2482 state: BreakpointState::Enabled,
2483 condition: None,
2484 hit_condition: None,
2485 };
2486
2487 let disable_breakpoint = Breakpoint {
2488 position: 578,
2489 message: None,
2490 state: BreakpointState::Disabled,
2491 condition: None,
2492 hit_condition: None,
2493 };
2494
2495 let condition_breakpoint = Breakpoint {
2496 position: 789,
2497 message: None,
2498 state: BreakpointState::Enabled,
2499 condition: Some("x > 5".into()),
2500 hit_condition: None,
2501 };
2502
2503 let hit_condition_breakpoint = Breakpoint {
2504 position: 999,
2505 message: None,
2506 state: BreakpointState::Enabled,
2507 condition: None,
2508 hit_condition: Some(">= 3".into()),
2509 };
2510
2511 let workspace = SerializedWorkspace {
2512 id,
2513 paths: PathList::new(&["/tmp"]),
2514 location: SerializedWorkspaceLocation::Local,
2515 center_group: Default::default(),
2516 window_bounds: Default::default(),
2517 display: Default::default(),
2518 docks: Default::default(),
2519 centered_layout: false,
2520 breakpoints: {
2521 let mut map = collections::BTreeMap::default();
2522 map.insert(
2523 Arc::from(path),
2524 vec![
2525 SourceBreakpoint {
2526 row: breakpoint.position,
2527 path: Arc::from(path),
2528 message: breakpoint.message.clone(),
2529 state: breakpoint.state,
2530 condition: breakpoint.condition.clone(),
2531 hit_condition: breakpoint.hit_condition.clone(),
2532 },
2533 SourceBreakpoint {
2534 row: log_breakpoint.position,
2535 path: Arc::from(path),
2536 message: log_breakpoint.message.clone(),
2537 state: log_breakpoint.state,
2538 condition: log_breakpoint.condition.clone(),
2539 hit_condition: log_breakpoint.hit_condition.clone(),
2540 },
2541 SourceBreakpoint {
2542 row: disable_breakpoint.position,
2543 path: Arc::from(path),
2544 message: disable_breakpoint.message.clone(),
2545 state: disable_breakpoint.state,
2546 condition: disable_breakpoint.condition.clone(),
2547 hit_condition: disable_breakpoint.hit_condition.clone(),
2548 },
2549 SourceBreakpoint {
2550 row: condition_breakpoint.position,
2551 path: Arc::from(path),
2552 message: condition_breakpoint.message.clone(),
2553 state: condition_breakpoint.state,
2554 condition: condition_breakpoint.condition.clone(),
2555 hit_condition: condition_breakpoint.hit_condition.clone(),
2556 },
2557 SourceBreakpoint {
2558 row: hit_condition_breakpoint.position,
2559 path: Arc::from(path),
2560 message: hit_condition_breakpoint.message.clone(),
2561 state: hit_condition_breakpoint.state,
2562 condition: hit_condition_breakpoint.condition.clone(),
2563 hit_condition: hit_condition_breakpoint.hit_condition.clone(),
2564 },
2565 ],
2566 );
2567 map
2568 },
2569 session_id: None,
2570 window_id: None,
2571 user_toolchains: Default::default(),
2572 };
2573
2574 db.save_workspace(workspace.clone()).await;
2575
2576 let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
2577 let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(path)).unwrap();
2578
2579 assert_eq!(loaded_breakpoints.len(), 5);
2580
2581 // normal breakpoint
2582 assert_eq!(loaded_breakpoints[0].row, breakpoint.position);
2583 assert_eq!(loaded_breakpoints[0].message, breakpoint.message);
2584 assert_eq!(loaded_breakpoints[0].condition, breakpoint.condition);
2585 assert_eq!(
2586 loaded_breakpoints[0].hit_condition,
2587 breakpoint.hit_condition
2588 );
2589 assert_eq!(loaded_breakpoints[0].state, breakpoint.state);
2590 assert_eq!(loaded_breakpoints[0].path, Arc::from(path));
2591
2592 // enabled breakpoint
2593 assert_eq!(loaded_breakpoints[1].row, log_breakpoint.position);
2594 assert_eq!(loaded_breakpoints[1].message, log_breakpoint.message);
2595 assert_eq!(loaded_breakpoints[1].condition, log_breakpoint.condition);
2596 assert_eq!(
2597 loaded_breakpoints[1].hit_condition,
2598 log_breakpoint.hit_condition
2599 );
2600 assert_eq!(loaded_breakpoints[1].state, log_breakpoint.state);
2601 assert_eq!(loaded_breakpoints[1].path, Arc::from(path));
2602
2603 // disable breakpoint
2604 assert_eq!(loaded_breakpoints[2].row, disable_breakpoint.position);
2605 assert_eq!(loaded_breakpoints[2].message, disable_breakpoint.message);
2606 assert_eq!(
2607 loaded_breakpoints[2].condition,
2608 disable_breakpoint.condition
2609 );
2610 assert_eq!(
2611 loaded_breakpoints[2].hit_condition,
2612 disable_breakpoint.hit_condition
2613 );
2614 assert_eq!(loaded_breakpoints[2].state, disable_breakpoint.state);
2615 assert_eq!(loaded_breakpoints[2].path, Arc::from(path));
2616
2617 // condition breakpoint
2618 assert_eq!(loaded_breakpoints[3].row, condition_breakpoint.position);
2619 assert_eq!(loaded_breakpoints[3].message, condition_breakpoint.message);
2620 assert_eq!(
2621 loaded_breakpoints[3].condition,
2622 condition_breakpoint.condition
2623 );
2624 assert_eq!(
2625 loaded_breakpoints[3].hit_condition,
2626 condition_breakpoint.hit_condition
2627 );
2628 assert_eq!(loaded_breakpoints[3].state, condition_breakpoint.state);
2629 assert_eq!(loaded_breakpoints[3].path, Arc::from(path));
2630
2631 // hit condition breakpoint
2632 assert_eq!(loaded_breakpoints[4].row, hit_condition_breakpoint.position);
2633 assert_eq!(
2634 loaded_breakpoints[4].message,
2635 hit_condition_breakpoint.message
2636 );
2637 assert_eq!(
2638 loaded_breakpoints[4].condition,
2639 hit_condition_breakpoint.condition
2640 );
2641 assert_eq!(
2642 loaded_breakpoints[4].hit_condition,
2643 hit_condition_breakpoint.hit_condition
2644 );
2645 assert_eq!(loaded_breakpoints[4].state, hit_condition_breakpoint.state);
2646 assert_eq!(loaded_breakpoints[4].path, Arc::from(path));
2647 }
2648
2649 #[gpui::test]
2650 async fn test_remove_last_breakpoint() {
2651 zlog::init_test();
2652
2653 let db = WorkspaceDb::open_test_db("test_remove_last_breakpoint").await;
2654 let id = db.next_id().await.unwrap();
2655
2656 let singular_path = Path::new("/tmp/test_remove_last_breakpoint.rs");
2657
2658 let breakpoint_to_remove = Breakpoint {
2659 position: 100,
2660 message: None,
2661 state: BreakpointState::Enabled,
2662 condition: None,
2663 hit_condition: None,
2664 };
2665
2666 let workspace = SerializedWorkspace {
2667 id,
2668 paths: PathList::new(&["/tmp"]),
2669 location: SerializedWorkspaceLocation::Local,
2670 center_group: Default::default(),
2671 window_bounds: Default::default(),
2672 display: Default::default(),
2673 docks: Default::default(),
2674 centered_layout: false,
2675 breakpoints: {
2676 let mut map = collections::BTreeMap::default();
2677 map.insert(
2678 Arc::from(singular_path),
2679 vec![SourceBreakpoint {
2680 row: breakpoint_to_remove.position,
2681 path: Arc::from(singular_path),
2682 message: None,
2683 state: BreakpointState::Enabled,
2684 condition: None,
2685 hit_condition: None,
2686 }],
2687 );
2688 map
2689 },
2690 session_id: None,
2691 window_id: None,
2692 user_toolchains: Default::default(),
2693 };
2694
2695 db.save_workspace(workspace.clone()).await;
2696
2697 let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
2698 let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(singular_path)).unwrap();
2699
2700 assert_eq!(loaded_breakpoints.len(), 1);
2701 assert_eq!(loaded_breakpoints[0].row, breakpoint_to_remove.position);
2702 assert_eq!(loaded_breakpoints[0].message, breakpoint_to_remove.message);
2703 assert_eq!(
2704 loaded_breakpoints[0].condition,
2705 breakpoint_to_remove.condition
2706 );
2707 assert_eq!(
2708 loaded_breakpoints[0].hit_condition,
2709 breakpoint_to_remove.hit_condition
2710 );
2711 assert_eq!(loaded_breakpoints[0].state, breakpoint_to_remove.state);
2712 assert_eq!(loaded_breakpoints[0].path, Arc::from(singular_path));
2713
2714 let workspace_without_breakpoint = SerializedWorkspace {
2715 id,
2716 paths: PathList::new(&["/tmp"]),
2717 location: SerializedWorkspaceLocation::Local,
2718 center_group: Default::default(),
2719 window_bounds: Default::default(),
2720 display: Default::default(),
2721 docks: Default::default(),
2722 centered_layout: false,
2723 breakpoints: collections::BTreeMap::default(),
2724 session_id: None,
2725 window_id: None,
2726 user_toolchains: Default::default(),
2727 };
2728
2729 db.save_workspace(workspace_without_breakpoint.clone())
2730 .await;
2731
2732 let loaded_after_remove = db.workspace_for_roots(&["/tmp"]).unwrap();
2733 let empty_breakpoints = loaded_after_remove
2734 .breakpoints
2735 .get(&Arc::from(singular_path));
2736
2737 assert!(empty_breakpoints.is_none());
2738 }
2739
2740 #[gpui::test]
2741 async fn test_next_id_stability() {
2742 zlog::init_test();
2743
2744 let db = WorkspaceDb::open_test_db("test_next_id_stability").await;
2745
2746 db.write(|conn| {
2747 conn.migrate(
2748 "test_table",
2749 &[sql!(
2750 CREATE TABLE test_table(
2751 text TEXT,
2752 workspace_id INTEGER,
2753 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
2754 ON DELETE CASCADE
2755 ) STRICT;
2756 )],
2757 &mut |_, _, _| false,
2758 )
2759 .unwrap();
2760 })
2761 .await;
2762
2763 let id = db.next_id().await.unwrap();
2764 // Assert the empty row got inserted
2765 assert_eq!(
2766 Some(id),
2767 db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
2768 SELECT workspace_id FROM workspaces WHERE workspace_id = ?
2769 ))
2770 .unwrap()(id)
2771 .unwrap()
2772 );
2773
2774 db.write(move |conn| {
2775 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2776 .unwrap()(("test-text-1", id))
2777 .unwrap()
2778 })
2779 .await;
2780
2781 let test_text_1 = db
2782 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2783 .unwrap()(1)
2784 .unwrap()
2785 .unwrap();
2786 assert_eq!(test_text_1, "test-text-1");
2787 }
2788
2789 #[gpui::test]
2790 async fn test_workspace_id_stability() {
2791 zlog::init_test();
2792
2793 let db = WorkspaceDb::open_test_db("test_workspace_id_stability").await;
2794
2795 db.write(|conn| {
2796 conn.migrate(
2797 "test_table",
2798 &[sql!(
2799 CREATE TABLE test_table(
2800 text TEXT,
2801 workspace_id INTEGER,
2802 FOREIGN KEY(workspace_id)
2803 REFERENCES workspaces(workspace_id)
2804 ON DELETE CASCADE
2805 ) STRICT;)],
2806 &mut |_, _, _| false,
2807 )
2808 })
2809 .await
2810 .unwrap();
2811
2812 let mut workspace_1 = SerializedWorkspace {
2813 id: WorkspaceId(1),
2814 paths: PathList::new(&["/tmp", "/tmp2"]),
2815 location: SerializedWorkspaceLocation::Local,
2816 center_group: Default::default(),
2817 window_bounds: Default::default(),
2818 display: Default::default(),
2819 docks: Default::default(),
2820 centered_layout: false,
2821 breakpoints: Default::default(),
2822 session_id: None,
2823 window_id: None,
2824 user_toolchains: Default::default(),
2825 };
2826
2827 let workspace_2 = SerializedWorkspace {
2828 id: WorkspaceId(2),
2829 paths: PathList::new(&["/tmp"]),
2830 location: SerializedWorkspaceLocation::Local,
2831 center_group: Default::default(),
2832 window_bounds: Default::default(),
2833 display: Default::default(),
2834 docks: Default::default(),
2835 centered_layout: false,
2836 breakpoints: Default::default(),
2837 session_id: None,
2838 window_id: None,
2839 user_toolchains: Default::default(),
2840 };
2841
2842 db.save_workspace(workspace_1.clone()).await;
2843
2844 db.write(|conn| {
2845 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2846 .unwrap()(("test-text-1", 1))
2847 .unwrap();
2848 })
2849 .await;
2850
2851 db.save_workspace(workspace_2.clone()).await;
2852
2853 db.write(|conn| {
2854 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2855 .unwrap()(("test-text-2", 2))
2856 .unwrap();
2857 })
2858 .await;
2859
2860 workspace_1.paths = PathList::new(&["/tmp", "/tmp3"]);
2861 db.save_workspace(workspace_1.clone()).await;
2862 db.save_workspace(workspace_1).await;
2863 db.save_workspace(workspace_2).await;
2864
2865 let test_text_2 = db
2866 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2867 .unwrap()(2)
2868 .unwrap()
2869 .unwrap();
2870 assert_eq!(test_text_2, "test-text-2");
2871
2872 let test_text_1 = db
2873 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2874 .unwrap()(1)
2875 .unwrap()
2876 .unwrap();
2877 assert_eq!(test_text_1, "test-text-1");
2878 }
2879
2880 fn group(axis: Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
2881 SerializedPaneGroup::Group {
2882 axis: SerializedAxis(axis),
2883 flexes: None,
2884 children,
2885 }
2886 }
2887
2888 #[gpui::test]
2889 async fn test_full_workspace_serialization() {
2890 zlog::init_test();
2891
2892 let db = WorkspaceDb::open_test_db("test_full_workspace_serialization").await;
2893
2894 // -----------------
2895 // | 1,2 | 5,6 |
2896 // | - - - | |
2897 // | 3,4 | |
2898 // -----------------
2899 let center_group = group(
2900 Axis::Horizontal,
2901 vec![
2902 group(
2903 Axis::Vertical,
2904 vec![
2905 SerializedPaneGroup::Pane(SerializedPane::new(
2906 vec![
2907 SerializedItem::new("Terminal", 5, false, false),
2908 SerializedItem::new("Terminal", 6, true, false),
2909 ],
2910 false,
2911 0,
2912 )),
2913 SerializedPaneGroup::Pane(SerializedPane::new(
2914 vec![
2915 SerializedItem::new("Terminal", 7, true, false),
2916 SerializedItem::new("Terminal", 8, false, false),
2917 ],
2918 false,
2919 0,
2920 )),
2921 ],
2922 ),
2923 SerializedPaneGroup::Pane(SerializedPane::new(
2924 vec![
2925 SerializedItem::new("Terminal", 9, false, false),
2926 SerializedItem::new("Terminal", 10, true, false),
2927 ],
2928 false,
2929 0,
2930 )),
2931 ],
2932 );
2933
2934 let workspace = SerializedWorkspace {
2935 id: WorkspaceId(5),
2936 paths: PathList::new(&["/tmp", "/tmp2"]),
2937 location: SerializedWorkspaceLocation::Local,
2938 center_group,
2939 window_bounds: Default::default(),
2940 breakpoints: Default::default(),
2941 display: Default::default(),
2942 docks: Default::default(),
2943 centered_layout: false,
2944 session_id: None,
2945 window_id: Some(999),
2946 user_toolchains: Default::default(),
2947 };
2948
2949 db.save_workspace(workspace.clone()).await;
2950
2951 let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
2952 assert_eq!(workspace, round_trip_workspace.unwrap());
2953
2954 // Test guaranteed duplicate IDs
2955 db.save_workspace(workspace.clone()).await;
2956 db.save_workspace(workspace.clone()).await;
2957
2958 let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
2959 assert_eq!(workspace, round_trip_workspace.unwrap());
2960 }
2961
2962 #[gpui::test]
2963 async fn test_workspace_assignment() {
2964 zlog::init_test();
2965
2966 let db = WorkspaceDb::open_test_db("test_basic_functionality").await;
2967
2968 let workspace_1 = SerializedWorkspace {
2969 id: WorkspaceId(1),
2970 paths: PathList::new(&["/tmp", "/tmp2"]),
2971 location: SerializedWorkspaceLocation::Local,
2972 center_group: Default::default(),
2973 window_bounds: Default::default(),
2974 breakpoints: Default::default(),
2975 display: Default::default(),
2976 docks: Default::default(),
2977 centered_layout: false,
2978 session_id: None,
2979 window_id: Some(1),
2980 user_toolchains: Default::default(),
2981 };
2982
2983 let mut workspace_2 = SerializedWorkspace {
2984 id: WorkspaceId(2),
2985 paths: PathList::new(&["/tmp"]),
2986 location: SerializedWorkspaceLocation::Local,
2987 center_group: Default::default(),
2988 window_bounds: Default::default(),
2989 display: Default::default(),
2990 docks: Default::default(),
2991 centered_layout: false,
2992 breakpoints: Default::default(),
2993 session_id: None,
2994 window_id: Some(2),
2995 user_toolchains: Default::default(),
2996 };
2997
2998 db.save_workspace(workspace_1.clone()).await;
2999 db.save_workspace(workspace_2.clone()).await;
3000
3001 // Test that paths are treated as a set
3002 assert_eq!(
3003 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
3004 workspace_1
3005 );
3006 assert_eq!(
3007 db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
3008 workspace_1
3009 );
3010
3011 // Make sure that other keys work
3012 assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
3013 assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
3014
3015 // Test 'mutate' case of updating a pre-existing id
3016 workspace_2.paths = PathList::new(&["/tmp", "/tmp2"]);
3017
3018 db.save_workspace(workspace_2.clone()).await;
3019 assert_eq!(
3020 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
3021 workspace_2
3022 );
3023
3024 // Test other mechanism for mutating
3025 let mut workspace_3 = SerializedWorkspace {
3026 id: WorkspaceId(3),
3027 paths: PathList::new(&["/tmp2", "/tmp"]),
3028 location: SerializedWorkspaceLocation::Local,
3029 center_group: Default::default(),
3030 window_bounds: Default::default(),
3031 breakpoints: Default::default(),
3032 display: Default::default(),
3033 docks: Default::default(),
3034 centered_layout: false,
3035 session_id: None,
3036 window_id: Some(3),
3037 user_toolchains: Default::default(),
3038 };
3039
3040 db.save_workspace(workspace_3.clone()).await;
3041 assert_eq!(
3042 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
3043 workspace_3
3044 );
3045
3046 // Make sure that updating paths differently also works
3047 workspace_3.paths = PathList::new(&["/tmp3", "/tmp4", "/tmp2"]);
3048 db.save_workspace(workspace_3.clone()).await;
3049 assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
3050 assert_eq!(
3051 db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
3052 .unwrap(),
3053 workspace_3
3054 );
3055 }
3056
3057 #[gpui::test]
3058 async fn test_session_workspaces() {
3059 zlog::init_test();
3060
3061 let db = WorkspaceDb::open_test_db("test_serializing_workspaces_session_id").await;
3062
3063 let workspace_1 = SerializedWorkspace {
3064 id: WorkspaceId(1),
3065 paths: PathList::new(&["/tmp1"]),
3066 location: SerializedWorkspaceLocation::Local,
3067 center_group: Default::default(),
3068 window_bounds: Default::default(),
3069 display: Default::default(),
3070 docks: Default::default(),
3071 centered_layout: false,
3072 breakpoints: Default::default(),
3073 session_id: Some("session-id-1".to_owned()),
3074 window_id: Some(10),
3075 user_toolchains: Default::default(),
3076 };
3077
3078 let workspace_2 = SerializedWorkspace {
3079 id: WorkspaceId(2),
3080 paths: PathList::new(&["/tmp2"]),
3081 location: SerializedWorkspaceLocation::Local,
3082 center_group: Default::default(),
3083 window_bounds: Default::default(),
3084 display: Default::default(),
3085 docks: Default::default(),
3086 centered_layout: false,
3087 breakpoints: Default::default(),
3088 session_id: Some("session-id-1".to_owned()),
3089 window_id: Some(20),
3090 user_toolchains: Default::default(),
3091 };
3092
3093 let workspace_3 = SerializedWorkspace {
3094 id: WorkspaceId(3),
3095 paths: PathList::new(&["/tmp3"]),
3096 location: SerializedWorkspaceLocation::Local,
3097 center_group: Default::default(),
3098 window_bounds: Default::default(),
3099 display: Default::default(),
3100 docks: Default::default(),
3101 centered_layout: false,
3102 breakpoints: Default::default(),
3103 session_id: Some("session-id-2".to_owned()),
3104 window_id: Some(30),
3105 user_toolchains: Default::default(),
3106 };
3107
3108 let workspace_4 = SerializedWorkspace {
3109 id: WorkspaceId(4),
3110 paths: PathList::new(&["/tmp4"]),
3111 location: SerializedWorkspaceLocation::Local,
3112 center_group: Default::default(),
3113 window_bounds: Default::default(),
3114 display: Default::default(),
3115 docks: Default::default(),
3116 centered_layout: false,
3117 breakpoints: Default::default(),
3118 session_id: None,
3119 window_id: None,
3120 user_toolchains: Default::default(),
3121 };
3122
3123 let connection_id = db
3124 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3125 host: "my-host".into(),
3126 port: Some(1234),
3127 ..Default::default()
3128 }))
3129 .await
3130 .unwrap();
3131
3132 let workspace_5 = SerializedWorkspace {
3133 id: WorkspaceId(5),
3134 paths: PathList::default(),
3135 location: SerializedWorkspaceLocation::Remote(
3136 db.remote_connection(connection_id).unwrap(),
3137 ),
3138 center_group: Default::default(),
3139 window_bounds: Default::default(),
3140 display: Default::default(),
3141 docks: Default::default(),
3142 centered_layout: false,
3143 breakpoints: Default::default(),
3144 session_id: Some("session-id-2".to_owned()),
3145 window_id: Some(50),
3146 user_toolchains: Default::default(),
3147 };
3148
3149 let workspace_6 = SerializedWorkspace {
3150 id: WorkspaceId(6),
3151 paths: PathList::new(&["/tmp6c", "/tmp6b", "/tmp6a"]),
3152 location: SerializedWorkspaceLocation::Local,
3153 center_group: Default::default(),
3154 window_bounds: Default::default(),
3155 breakpoints: Default::default(),
3156 display: Default::default(),
3157 docks: Default::default(),
3158 centered_layout: false,
3159 session_id: Some("session-id-3".to_owned()),
3160 window_id: Some(60),
3161 user_toolchains: Default::default(),
3162 };
3163
3164 db.save_workspace(workspace_1.clone()).await;
3165 thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
3166 db.save_workspace(workspace_2.clone()).await;
3167 db.save_workspace(workspace_3.clone()).await;
3168 thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
3169 db.save_workspace(workspace_4.clone()).await;
3170 db.save_workspace(workspace_5.clone()).await;
3171 db.save_workspace(workspace_6.clone()).await;
3172
3173 let locations = db.session_workspaces("session-id-1".to_owned()).unwrap();
3174 assert_eq!(locations.len(), 2);
3175 assert_eq!(locations[0].0, WorkspaceId(2));
3176 assert_eq!(locations[0].1, PathList::new(&["/tmp2"]));
3177 assert_eq!(locations[0].2, Some(20));
3178 assert_eq!(locations[1].0, WorkspaceId(1));
3179 assert_eq!(locations[1].1, PathList::new(&["/tmp1"]));
3180 assert_eq!(locations[1].2, Some(10));
3181
3182 let locations = db.session_workspaces("session-id-2".to_owned()).unwrap();
3183 assert_eq!(locations.len(), 2);
3184 assert_eq!(locations[0].0, WorkspaceId(5));
3185 assert_eq!(locations[0].1, PathList::default());
3186 assert_eq!(locations[0].2, Some(50));
3187 assert_eq!(locations[0].3, Some(connection_id));
3188 assert_eq!(locations[1].0, WorkspaceId(3));
3189 assert_eq!(locations[1].1, PathList::new(&["/tmp3"]));
3190 assert_eq!(locations[1].2, Some(30));
3191
3192 let locations = db.session_workspaces("session-id-3".to_owned()).unwrap();
3193 assert_eq!(locations.len(), 1);
3194 assert_eq!(locations[0].0, WorkspaceId(6));
3195 assert_eq!(
3196 locations[0].1,
3197 PathList::new(&["/tmp6c", "/tmp6b", "/tmp6a"]),
3198 );
3199 assert_eq!(locations[0].2, Some(60));
3200 }
3201
3202 fn default_workspace<P: AsRef<Path>>(
3203 paths: &[P],
3204 center_group: &SerializedPaneGroup,
3205 ) -> SerializedWorkspace {
3206 SerializedWorkspace {
3207 id: WorkspaceId(4),
3208 paths: PathList::new(paths),
3209 location: SerializedWorkspaceLocation::Local,
3210 center_group: center_group.clone(),
3211 window_bounds: Default::default(),
3212 display: Default::default(),
3213 docks: Default::default(),
3214 breakpoints: Default::default(),
3215 centered_layout: false,
3216 session_id: None,
3217 window_id: None,
3218 user_toolchains: Default::default(),
3219 }
3220 }
3221
3222 #[gpui::test]
3223 async fn test_last_session_workspace_locations(cx: &mut gpui::TestAppContext) {
3224 let dir1 = tempfile::TempDir::with_prefix("dir1").unwrap();
3225 let dir2 = tempfile::TempDir::with_prefix("dir2").unwrap();
3226 let dir3 = tempfile::TempDir::with_prefix("dir3").unwrap();
3227 let dir4 = tempfile::TempDir::with_prefix("dir4").unwrap();
3228
3229 let fs = fs::FakeFs::new(cx.executor());
3230 fs.insert_tree(dir1.path(), json!({})).await;
3231 fs.insert_tree(dir2.path(), json!({})).await;
3232 fs.insert_tree(dir3.path(), json!({})).await;
3233 fs.insert_tree(dir4.path(), json!({})).await;
3234
3235 let db =
3236 WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces").await;
3237
3238 let workspaces = [
3239 (1, vec![dir1.path()], 9),
3240 (2, vec![dir2.path()], 5),
3241 (3, vec![dir3.path()], 8),
3242 (4, vec![dir4.path()], 2),
3243 (5, vec![dir1.path(), dir2.path(), dir3.path()], 3),
3244 (6, vec![dir4.path(), dir3.path(), dir2.path()], 4),
3245 ]
3246 .into_iter()
3247 .map(|(id, paths, window_id)| SerializedWorkspace {
3248 id: WorkspaceId(id),
3249 paths: PathList::new(paths.as_slice()),
3250 location: SerializedWorkspaceLocation::Local,
3251 center_group: Default::default(),
3252 window_bounds: Default::default(),
3253 display: Default::default(),
3254 docks: Default::default(),
3255 centered_layout: false,
3256 session_id: Some("one-session".to_owned()),
3257 breakpoints: Default::default(),
3258 window_id: Some(window_id),
3259 user_toolchains: Default::default(),
3260 })
3261 .collect::<Vec<_>>();
3262
3263 for workspace in workspaces.iter() {
3264 db.save_workspace(workspace.clone()).await;
3265 }
3266
3267 let stack = Some(Vec::from([
3268 WindowId::from(2), // Top
3269 WindowId::from(8),
3270 WindowId::from(5),
3271 WindowId::from(9),
3272 WindowId::from(3),
3273 WindowId::from(4), // Bottom
3274 ]));
3275
3276 let locations = db
3277 .last_session_workspace_locations("one-session", stack, fs.as_ref())
3278 .await
3279 .unwrap();
3280 assert_eq!(
3281 locations,
3282 [
3283 SessionWorkspace {
3284 workspace_id: WorkspaceId(4),
3285 location: SerializedWorkspaceLocation::Local,
3286 paths: PathList::new(&[dir4.path()]),
3287 window_id: Some(WindowId::from(2u64)),
3288 },
3289 SessionWorkspace {
3290 workspace_id: WorkspaceId(3),
3291 location: SerializedWorkspaceLocation::Local,
3292 paths: PathList::new(&[dir3.path()]),
3293 window_id: Some(WindowId::from(8u64)),
3294 },
3295 SessionWorkspace {
3296 workspace_id: WorkspaceId(2),
3297 location: SerializedWorkspaceLocation::Local,
3298 paths: PathList::new(&[dir2.path()]),
3299 window_id: Some(WindowId::from(5u64)),
3300 },
3301 SessionWorkspace {
3302 workspace_id: WorkspaceId(1),
3303 location: SerializedWorkspaceLocation::Local,
3304 paths: PathList::new(&[dir1.path()]),
3305 window_id: Some(WindowId::from(9u64)),
3306 },
3307 SessionWorkspace {
3308 workspace_id: WorkspaceId(5),
3309 location: SerializedWorkspaceLocation::Local,
3310 paths: PathList::new(&[dir1.path(), dir2.path(), dir3.path()]),
3311 window_id: Some(WindowId::from(3u64)),
3312 },
3313 SessionWorkspace {
3314 workspace_id: WorkspaceId(6),
3315 location: SerializedWorkspaceLocation::Local,
3316 paths: PathList::new(&[dir4.path(), dir3.path(), dir2.path()]),
3317 window_id: Some(WindowId::from(4u64)),
3318 },
3319 ]
3320 );
3321 }
3322
3323 #[gpui::test]
3324 async fn test_last_session_workspace_locations_remote(cx: &mut gpui::TestAppContext) {
3325 let fs = fs::FakeFs::new(cx.executor());
3326 let db =
3327 WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces_remote")
3328 .await;
3329
3330 let remote_connections = [
3331 ("host-1", "my-user-1"),
3332 ("host-2", "my-user-2"),
3333 ("host-3", "my-user-3"),
3334 ("host-4", "my-user-4"),
3335 ]
3336 .into_iter()
3337 .map(|(host, user)| async {
3338 let options = RemoteConnectionOptions::Ssh(SshConnectionOptions {
3339 host: host.into(),
3340 username: Some(user.to_string()),
3341 ..Default::default()
3342 });
3343 db.get_or_create_remote_connection(options.clone())
3344 .await
3345 .unwrap();
3346 options
3347 })
3348 .collect::<Vec<_>>();
3349
3350 let remote_connections = futures::future::join_all(remote_connections).await;
3351
3352 let workspaces = [
3353 (1, remote_connections[0].clone(), 9),
3354 (2, remote_connections[1].clone(), 5),
3355 (3, remote_connections[2].clone(), 8),
3356 (4, remote_connections[3].clone(), 2),
3357 ]
3358 .into_iter()
3359 .map(|(id, remote_connection, window_id)| SerializedWorkspace {
3360 id: WorkspaceId(id),
3361 paths: PathList::default(),
3362 location: SerializedWorkspaceLocation::Remote(remote_connection),
3363 center_group: Default::default(),
3364 window_bounds: Default::default(),
3365 display: Default::default(),
3366 docks: Default::default(),
3367 centered_layout: false,
3368 session_id: Some("one-session".to_owned()),
3369 breakpoints: Default::default(),
3370 window_id: Some(window_id),
3371 user_toolchains: Default::default(),
3372 })
3373 .collect::<Vec<_>>();
3374
3375 for workspace in workspaces.iter() {
3376 db.save_workspace(workspace.clone()).await;
3377 }
3378
3379 let stack = Some(Vec::from([
3380 WindowId::from(2), // Top
3381 WindowId::from(8),
3382 WindowId::from(5),
3383 WindowId::from(9), // Bottom
3384 ]));
3385
3386 let have = db
3387 .last_session_workspace_locations("one-session", stack, fs.as_ref())
3388 .await
3389 .unwrap();
3390 assert_eq!(have.len(), 4);
3391 assert_eq!(
3392 have[0],
3393 SessionWorkspace {
3394 workspace_id: WorkspaceId(4),
3395 location: SerializedWorkspaceLocation::Remote(remote_connections[3].clone()),
3396 paths: PathList::default(),
3397 window_id: Some(WindowId::from(2u64)),
3398 }
3399 );
3400 assert_eq!(
3401 have[1],
3402 SessionWorkspace {
3403 workspace_id: WorkspaceId(3),
3404 location: SerializedWorkspaceLocation::Remote(remote_connections[2].clone()),
3405 paths: PathList::default(),
3406 window_id: Some(WindowId::from(8u64)),
3407 }
3408 );
3409 assert_eq!(
3410 have[2],
3411 SessionWorkspace {
3412 workspace_id: WorkspaceId(2),
3413 location: SerializedWorkspaceLocation::Remote(remote_connections[1].clone()),
3414 paths: PathList::default(),
3415 window_id: Some(WindowId::from(5u64)),
3416 }
3417 );
3418 assert_eq!(
3419 have[3],
3420 SessionWorkspace {
3421 workspace_id: WorkspaceId(1),
3422 location: SerializedWorkspaceLocation::Remote(remote_connections[0].clone()),
3423 paths: PathList::default(),
3424 window_id: Some(WindowId::from(9u64)),
3425 }
3426 );
3427 }
3428
3429 #[gpui::test]
3430 async fn test_get_or_create_ssh_project() {
3431 let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project").await;
3432
3433 let host = "example.com".to_string();
3434 let port = Some(22_u16);
3435 let user = Some("user".to_string());
3436
3437 let connection_id = db
3438 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3439 host: host.clone().into(),
3440 port,
3441 username: user.clone(),
3442 ..Default::default()
3443 }))
3444 .await
3445 .unwrap();
3446
3447 // Test that calling the function again with the same parameters returns the same project
3448 let same_connection = db
3449 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3450 host: host.clone().into(),
3451 port,
3452 username: user.clone(),
3453 ..Default::default()
3454 }))
3455 .await
3456 .unwrap();
3457
3458 assert_eq!(connection_id, same_connection);
3459
3460 // Test with different parameters
3461 let host2 = "otherexample.com".to_string();
3462 let port2 = None;
3463 let user2 = Some("otheruser".to_string());
3464
3465 let different_connection = db
3466 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3467 host: host2.clone().into(),
3468 port: port2,
3469 username: user2.clone(),
3470 ..Default::default()
3471 }))
3472 .await
3473 .unwrap();
3474
3475 assert_ne!(connection_id, different_connection);
3476 }
3477
3478 #[gpui::test]
3479 async fn test_get_or_create_ssh_project_with_null_user() {
3480 let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project_with_null_user").await;
3481
3482 let (host, port, user) = ("example.com".to_string(), None, None);
3483
3484 let connection_id = db
3485 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3486 host: host.clone().into(),
3487 port,
3488 username: None,
3489 ..Default::default()
3490 }))
3491 .await
3492 .unwrap();
3493
3494 let same_connection_id = db
3495 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3496 host: host.clone().into(),
3497 port,
3498 username: user.clone(),
3499 ..Default::default()
3500 }))
3501 .await
3502 .unwrap();
3503
3504 assert_eq!(connection_id, same_connection_id);
3505 }
3506
3507 #[gpui::test]
3508 async fn test_get_remote_connections() {
3509 let db = WorkspaceDb::open_test_db("test_get_remote_connections").await;
3510
3511 let connections = [
3512 ("example.com".to_string(), None, None),
3513 (
3514 "anotherexample.com".to_string(),
3515 Some(123_u16),
3516 Some("user2".to_string()),
3517 ),
3518 ("yetanother.com".to_string(), Some(345_u16), None),
3519 ];
3520
3521 let mut ids = Vec::new();
3522 for (host, port, user) in connections.iter() {
3523 ids.push(
3524 db.get_or_create_remote_connection(RemoteConnectionOptions::Ssh(
3525 SshConnectionOptions {
3526 host: host.clone().into(),
3527 port: *port,
3528 username: user.clone(),
3529 ..Default::default()
3530 },
3531 ))
3532 .await
3533 .unwrap(),
3534 );
3535 }
3536
3537 let stored_connections = db.remote_connections().unwrap();
3538 assert_eq!(
3539 stored_connections,
3540 [
3541 (
3542 ids[0],
3543 RemoteConnectionOptions::Ssh(SshConnectionOptions {
3544 host: "example.com".into(),
3545 port: None,
3546 username: None,
3547 ..Default::default()
3548 }),
3549 ),
3550 (
3551 ids[1],
3552 RemoteConnectionOptions::Ssh(SshConnectionOptions {
3553 host: "anotherexample.com".into(),
3554 port: Some(123),
3555 username: Some("user2".into()),
3556 ..Default::default()
3557 }),
3558 ),
3559 (
3560 ids[2],
3561 RemoteConnectionOptions::Ssh(SshConnectionOptions {
3562 host: "yetanother.com".into(),
3563 port: Some(345),
3564 username: None,
3565 ..Default::default()
3566 }),
3567 ),
3568 ]
3569 .into_iter()
3570 .collect::<HashMap<_, _>>(),
3571 );
3572 }
3573
3574 #[gpui::test]
3575 async fn test_simple_split() {
3576 zlog::init_test();
3577
3578 let db = WorkspaceDb::open_test_db("simple_split").await;
3579
3580 // -----------------
3581 // | 1,2 | 5,6 |
3582 // | - - - | |
3583 // | 3,4 | |
3584 // -----------------
3585 let center_pane = group(
3586 Axis::Horizontal,
3587 vec![
3588 group(
3589 Axis::Vertical,
3590 vec![
3591 SerializedPaneGroup::Pane(SerializedPane::new(
3592 vec![
3593 SerializedItem::new("Terminal", 1, false, false),
3594 SerializedItem::new("Terminal", 2, true, false),
3595 ],
3596 false,
3597 0,
3598 )),
3599 SerializedPaneGroup::Pane(SerializedPane::new(
3600 vec![
3601 SerializedItem::new("Terminal", 4, false, false),
3602 SerializedItem::new("Terminal", 3, true, false),
3603 ],
3604 true,
3605 0,
3606 )),
3607 ],
3608 ),
3609 SerializedPaneGroup::Pane(SerializedPane::new(
3610 vec![
3611 SerializedItem::new("Terminal", 5, true, false),
3612 SerializedItem::new("Terminal", 6, false, false),
3613 ],
3614 false,
3615 0,
3616 )),
3617 ],
3618 );
3619
3620 let workspace = default_workspace(&["/tmp"], ¢er_pane);
3621
3622 db.save_workspace(workspace.clone()).await;
3623
3624 let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
3625
3626 assert_eq!(workspace.center_group, new_workspace.center_group);
3627 }
3628
3629 #[gpui::test]
3630 async fn test_cleanup_panes() {
3631 zlog::init_test();
3632
3633 let db = WorkspaceDb::open_test_db("test_cleanup_panes").await;
3634
3635 let center_pane = group(
3636 Axis::Horizontal,
3637 vec![
3638 group(
3639 Axis::Vertical,
3640 vec![
3641 SerializedPaneGroup::Pane(SerializedPane::new(
3642 vec![
3643 SerializedItem::new("Terminal", 1, false, false),
3644 SerializedItem::new("Terminal", 2, true, false),
3645 ],
3646 false,
3647 0,
3648 )),
3649 SerializedPaneGroup::Pane(SerializedPane::new(
3650 vec![
3651 SerializedItem::new("Terminal", 4, false, false),
3652 SerializedItem::new("Terminal", 3, true, false),
3653 ],
3654 true,
3655 0,
3656 )),
3657 ],
3658 ),
3659 SerializedPaneGroup::Pane(SerializedPane::new(
3660 vec![
3661 SerializedItem::new("Terminal", 5, false, false),
3662 SerializedItem::new("Terminal", 6, true, false),
3663 ],
3664 false,
3665 0,
3666 )),
3667 ],
3668 );
3669
3670 let id = &["/tmp"];
3671
3672 let mut workspace = default_workspace(id, ¢er_pane);
3673
3674 db.save_workspace(workspace.clone()).await;
3675
3676 workspace.center_group = group(
3677 Axis::Vertical,
3678 vec![
3679 SerializedPaneGroup::Pane(SerializedPane::new(
3680 vec![
3681 SerializedItem::new("Terminal", 1, false, false),
3682 SerializedItem::new("Terminal", 2, true, false),
3683 ],
3684 false,
3685 0,
3686 )),
3687 SerializedPaneGroup::Pane(SerializedPane::new(
3688 vec![
3689 SerializedItem::new("Terminal", 4, true, false),
3690 SerializedItem::new("Terminal", 3, false, false),
3691 ],
3692 true,
3693 0,
3694 )),
3695 ],
3696 );
3697
3698 db.save_workspace(workspace.clone()).await;
3699
3700 let new_workspace = db.workspace_for_roots(id).unwrap();
3701
3702 assert_eq!(workspace.center_group, new_workspace.center_group);
3703 }
3704
3705 #[gpui::test]
3706 async fn test_empty_workspace_window_bounds() {
3707 zlog::init_test();
3708
3709 let db = WorkspaceDb::open_test_db("test_empty_workspace_window_bounds").await;
3710 let id = db.next_id().await.unwrap();
3711
3712 // Create a workspace with empty paths (empty workspace)
3713 let empty_paths: &[&str] = &[];
3714 let display_uuid = Uuid::new_v4();
3715 let window_bounds = SerializedWindowBounds(WindowBounds::Windowed(Bounds {
3716 origin: point(px(100.0), px(200.0)),
3717 size: size(px(800.0), px(600.0)),
3718 }));
3719
3720 let workspace = SerializedWorkspace {
3721 id,
3722 paths: PathList::new(empty_paths),
3723 location: SerializedWorkspaceLocation::Local,
3724 center_group: Default::default(),
3725 window_bounds: None,
3726 display: None,
3727 docks: Default::default(),
3728 breakpoints: Default::default(),
3729 centered_layout: false,
3730 session_id: None,
3731 window_id: None,
3732 user_toolchains: Default::default(),
3733 };
3734
3735 // Save the workspace (this creates the record with empty paths)
3736 db.save_workspace(workspace.clone()).await;
3737
3738 // Save window bounds separately (as the actual code does via set_window_open_status)
3739 db.set_window_open_status(id, window_bounds, display_uuid)
3740 .await
3741 .unwrap();
3742
3743 // Empty workspaces cannot be retrieved by paths (they'd all match).
3744 // They must be retrieved by workspace_id.
3745 assert!(db.workspace_for_roots(empty_paths).is_none());
3746
3747 // Retrieve using workspace_for_id instead
3748 let retrieved = db.workspace_for_id(id).unwrap();
3749
3750 // Verify window bounds were persisted
3751 assert_eq!(retrieved.id, id);
3752 assert!(retrieved.window_bounds.is_some());
3753 assert_eq!(retrieved.window_bounds.unwrap().0, window_bounds.0);
3754 assert!(retrieved.display.is_some());
3755 assert_eq!(retrieved.display.unwrap(), display_uuid);
3756 }
3757
3758 #[gpui::test]
3759 async fn test_last_session_workspace_locations_groups_by_window_id(
3760 cx: &mut gpui::TestAppContext,
3761 ) {
3762 let dir1 = tempfile::TempDir::with_prefix("dir1").unwrap();
3763 let dir2 = tempfile::TempDir::with_prefix("dir2").unwrap();
3764 let dir3 = tempfile::TempDir::with_prefix("dir3").unwrap();
3765 let dir4 = tempfile::TempDir::with_prefix("dir4").unwrap();
3766 let dir5 = tempfile::TempDir::with_prefix("dir5").unwrap();
3767
3768 let fs = fs::FakeFs::new(cx.executor());
3769 fs.insert_tree(dir1.path(), json!({})).await;
3770 fs.insert_tree(dir2.path(), json!({})).await;
3771 fs.insert_tree(dir3.path(), json!({})).await;
3772 fs.insert_tree(dir4.path(), json!({})).await;
3773 fs.insert_tree(dir5.path(), json!({})).await;
3774
3775 let db =
3776 WorkspaceDb::open_test_db("test_last_session_workspace_locations_groups_by_window_id")
3777 .await;
3778
3779 // Simulate two MultiWorkspace windows each containing two workspaces,
3780 // plus one single-workspace window:
3781 // Window 10: workspace 1, workspace 2
3782 // Window 20: workspace 3, workspace 4
3783 // Window 30: workspace 5 (only one)
3784 //
3785 // On session restore, the caller should be able to group these by
3786 // window_id to reconstruct the MultiWorkspace windows.
3787 let workspaces_data: Vec<(i64, &Path, u64)> = vec![
3788 (1, dir1.path(), 10),
3789 (2, dir2.path(), 10),
3790 (3, dir3.path(), 20),
3791 (4, dir4.path(), 20),
3792 (5, dir5.path(), 30),
3793 ];
3794
3795 for (id, dir, window_id) in &workspaces_data {
3796 db.save_workspace(SerializedWorkspace {
3797 id: WorkspaceId(*id),
3798 paths: PathList::new(&[*dir]),
3799 location: SerializedWorkspaceLocation::Local,
3800 center_group: Default::default(),
3801 window_bounds: Default::default(),
3802 display: Default::default(),
3803 docks: Default::default(),
3804 centered_layout: false,
3805 session_id: Some("test-session".to_owned()),
3806 breakpoints: Default::default(),
3807 window_id: Some(*window_id),
3808 user_toolchains: Default::default(),
3809 })
3810 .await;
3811 }
3812
3813 let locations = db
3814 .last_session_workspace_locations("test-session", None, fs.as_ref())
3815 .await
3816 .unwrap();
3817
3818 // All 5 workspaces should be returned with their window_ids.
3819 assert_eq!(locations.len(), 5);
3820
3821 // Every entry should have a window_id so the caller can group them.
3822 for session_workspace in &locations {
3823 assert!(
3824 session_workspace.window_id.is_some(),
3825 "workspace {:?} missing window_id",
3826 session_workspace.workspace_id
3827 );
3828 }
3829
3830 // Group by window_id, simulating what the restoration code should do.
3831 let mut by_window: HashMap<WindowId, Vec<WorkspaceId>> = HashMap::default();
3832 for session_workspace in &locations {
3833 if let Some(window_id) = session_workspace.window_id {
3834 by_window
3835 .entry(window_id)
3836 .or_default()
3837 .push(session_workspace.workspace_id);
3838 }
3839 }
3840
3841 // Should produce 3 windows, not 5.
3842 assert_eq!(
3843 by_window.len(),
3844 3,
3845 "Expected 3 window groups, got {}: {:?}",
3846 by_window.len(),
3847 by_window
3848 );
3849
3850 // Window 10 should contain workspaces 1 and 2.
3851 let window_10 = by_window.get(&WindowId::from(10u64)).unwrap();
3852 assert_eq!(window_10.len(), 2);
3853 assert!(window_10.contains(&WorkspaceId(1)));
3854 assert!(window_10.contains(&WorkspaceId(2)));
3855
3856 // Window 20 should contain workspaces 3 and 4.
3857 let window_20 = by_window.get(&WindowId::from(20u64)).unwrap();
3858 assert_eq!(window_20.len(), 2);
3859 assert!(window_20.contains(&WorkspaceId(3)));
3860 assert!(window_20.contains(&WorkspaceId(4)));
3861
3862 // Window 30 should contain only workspace 5.
3863 let window_30 = by_window.get(&WindowId::from(30u64)).unwrap();
3864 assert_eq!(window_30.len(), 1);
3865 assert!(window_30.contains(&WorkspaceId(5)));
3866 }
3867
3868 #[gpui::test]
3869 async fn test_read_serialized_multi_workspaces_with_state() {
3870 use crate::persistence::model::MultiWorkspaceState;
3871
3872 // Write multi-workspace state for two windows via the scoped KVP.
3873 let window_10 = WindowId::from(10u64);
3874 let window_20 = WindowId::from(20u64);
3875
3876 write_multi_workspace_state(
3877 window_10,
3878 MultiWorkspaceState {
3879 active_workspace_id: Some(WorkspaceId(2)),
3880 sidebar_open: true,
3881 },
3882 )
3883 .await;
3884
3885 write_multi_workspace_state(
3886 window_20,
3887 MultiWorkspaceState {
3888 active_workspace_id: Some(WorkspaceId(3)),
3889 sidebar_open: false,
3890 },
3891 )
3892 .await;
3893
3894 // Build session workspaces: two in window 10, one in window 20, one with no window.
3895 let session_workspaces = vec![
3896 SessionWorkspace {
3897 workspace_id: WorkspaceId(1),
3898 location: SerializedWorkspaceLocation::Local,
3899 paths: PathList::new(&["/a"]),
3900 window_id: Some(window_10),
3901 },
3902 SessionWorkspace {
3903 workspace_id: WorkspaceId(2),
3904 location: SerializedWorkspaceLocation::Local,
3905 paths: PathList::new(&["/b"]),
3906 window_id: Some(window_10),
3907 },
3908 SessionWorkspace {
3909 workspace_id: WorkspaceId(3),
3910 location: SerializedWorkspaceLocation::Local,
3911 paths: PathList::new(&["/c"]),
3912 window_id: Some(window_20),
3913 },
3914 SessionWorkspace {
3915 workspace_id: WorkspaceId(4),
3916 location: SerializedWorkspaceLocation::Local,
3917 paths: PathList::new(&["/d"]),
3918 window_id: None,
3919 },
3920 ];
3921
3922 let results = read_serialized_multi_workspaces(session_workspaces);
3923
3924 // Should produce 3 groups: window 10, window 20, and the orphan.
3925 assert_eq!(results.len(), 3);
3926
3927 // Window 10 group: 2 workspaces, active_workspace_id = 2, sidebar open.
3928 let group_10 = &results[0];
3929 assert_eq!(group_10.workspaces.len(), 2);
3930 assert_eq!(group_10.state.active_workspace_id, Some(WorkspaceId(2)));
3931 assert_eq!(group_10.state.sidebar_open, true);
3932
3933 // Window 20 group: 1 workspace, active_workspace_id = 3, sidebar closed.
3934 let group_20 = &results[1];
3935 assert_eq!(group_20.workspaces.len(), 1);
3936 assert_eq!(group_20.state.active_workspace_id, Some(WorkspaceId(3)));
3937 assert_eq!(group_20.state.sidebar_open, false);
3938
3939 // Orphan group: no window_id, so state is default.
3940 let group_none = &results[2];
3941 assert_eq!(group_none.workspaces.len(), 1);
3942 assert_eq!(group_none.state.active_workspace_id, None);
3943 assert_eq!(group_none.state.sidebar_open, false);
3944 }
3945
3946 #[gpui::test]
3947 async fn test_flush_serialization_completes_before_quit(cx: &mut gpui::TestAppContext) {
3948 use crate::multi_workspace::MultiWorkspace;
3949 use feature_flags::FeatureFlagAppExt;
3950
3951 use project::Project;
3952
3953 crate::tests::init_test(cx);
3954
3955 cx.update(|cx| {
3956 cx.set_staff(true);
3957 cx.update_flags(true, vec!["agent-v2".to_string()]);
3958 });
3959
3960 let fs = fs::FakeFs::new(cx.executor());
3961 let project = Project::test(fs.clone(), [], cx).await;
3962
3963 let (multi_workspace, cx) =
3964 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3965
3966 let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
3967
3968 // Assign a database_id so serialization will actually persist.
3969 let workspace_id = DB.next_id().await.unwrap();
3970 workspace.update(cx, |ws, _cx| {
3971 ws.set_database_id(workspace_id);
3972 });
3973
3974 // Mutate some workspace state.
3975 DB.set_centered_layout(workspace_id, true).await.unwrap();
3976
3977 // Call flush_serialization and await the returned task directly
3978 // (without run_until_parked — the point is that awaiting the task
3979 // alone is sufficient).
3980 let task = multi_workspace.update_in(cx, |mw, window, cx| {
3981 mw.workspace()
3982 .update(cx, |ws, cx| ws.flush_serialization(window, cx))
3983 });
3984 task.await;
3985
3986 // Read the workspace back from the DB and verify serialization happened.
3987 let serialized = DB.workspace_for_id(workspace_id);
3988 assert!(
3989 serialized.is_some(),
3990 "flush_serialization should have persisted the workspace to DB"
3991 );
3992 }
3993
3994 #[gpui::test]
3995 async fn test_create_workspace_serializes_active_workspace_id_after_db_id_assigned(
3996 cx: &mut gpui::TestAppContext,
3997 ) {
3998 use crate::multi_workspace::MultiWorkspace;
3999 use crate::persistence::read_multi_workspace_state;
4000 use feature_flags::FeatureFlagAppExt;
4001
4002 use project::Project;
4003
4004 crate::tests::init_test(cx);
4005
4006 cx.update(|cx| {
4007 cx.set_staff(true);
4008 cx.update_flags(true, vec!["agent-v2".to_string()]);
4009 });
4010
4011 let fs = fs::FakeFs::new(cx.executor());
4012 let project = Project::test(fs.clone(), [], cx).await;
4013
4014 let (multi_workspace, cx) =
4015 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
4016
4017 // Give the first workspace a database_id.
4018 multi_workspace.update_in(cx, |mw, _, cx| {
4019 mw.set_random_database_id(cx);
4020 });
4021
4022 let window_id =
4023 multi_workspace.update_in(cx, |_, window, _cx| window.window_handle().window_id());
4024
4025 // Create a new workspace via the MultiWorkspace API (triggers next_id()).
4026 multi_workspace.update_in(cx, |mw, window, cx| {
4027 mw.create_workspace(window, cx);
4028 });
4029
4030 // Let the async next_id() and re-serialization tasks complete.
4031 cx.run_until_parked();
4032
4033 // Read back the multi-workspace state.
4034 let state = read_multi_workspace_state(window_id);
4035
4036 // The new workspace should now have a database_id, and the multi-workspace
4037 // state should record it as the active workspace.
4038 let new_workspace_db_id =
4039 multi_workspace.read_with(cx, |mw, cx| mw.workspace().read(cx).database_id());
4040 assert!(
4041 new_workspace_db_id.is_some(),
4042 "New workspace should have a database_id after run_until_parked"
4043 );
4044 assert_eq!(
4045 state.active_workspace_id, new_workspace_db_id,
4046 "Serialized active_workspace_id should match the new workspace's database_id"
4047 );
4048 }
4049
4050 #[gpui::test]
4051 async fn test_create_workspace_individual_serialization(cx: &mut gpui::TestAppContext) {
4052 use crate::multi_workspace::MultiWorkspace;
4053 use feature_flags::FeatureFlagAppExt;
4054
4055 use project::Project;
4056
4057 crate::tests::init_test(cx);
4058
4059 cx.update(|cx| {
4060 cx.set_staff(true);
4061 cx.update_flags(true, vec!["agent-v2".to_string()]);
4062 });
4063
4064 let fs = fs::FakeFs::new(cx.executor());
4065 let project = Project::test(fs.clone(), [], cx).await;
4066
4067 let (multi_workspace, cx) =
4068 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
4069
4070 multi_workspace.update_in(cx, |mw, _, cx| {
4071 mw.set_random_database_id(cx);
4072 });
4073
4074 // Create a new workspace.
4075 multi_workspace.update_in(cx, |mw, window, cx| {
4076 mw.create_workspace(window, cx);
4077 });
4078
4079 cx.run_until_parked();
4080
4081 // Get the new workspace's database_id.
4082 let new_db_id =
4083 multi_workspace.read_with(cx, |mw, cx| mw.workspace().read(cx).database_id());
4084 assert!(
4085 new_db_id.is_some(),
4086 "New workspace should have a database_id"
4087 );
4088
4089 let workspace_id = new_db_id.unwrap();
4090
4091 // The workspace should have been serialized to the DB with real data
4092 // (not just the bare DEFAULT VALUES row from next_id).
4093 let serialized = DB.workspace_for_id(workspace_id);
4094 assert!(
4095 serialized.is_some(),
4096 "Newly created workspace should be fully serialized in the DB after database_id assignment"
4097 );
4098 }
4099
4100 #[gpui::test]
4101 async fn test_remove_workspace_deletes_db_row(cx: &mut gpui::TestAppContext) {
4102 use crate::multi_workspace::MultiWorkspace;
4103 use feature_flags::FeatureFlagAppExt;
4104 use gpui::AppContext as _;
4105 use project::Project;
4106
4107 crate::tests::init_test(cx);
4108
4109 cx.update(|cx| {
4110 cx.set_staff(true);
4111 cx.update_flags(true, vec!["agent-v2".to_string()]);
4112 });
4113
4114 let fs = fs::FakeFs::new(cx.executor());
4115 let project1 = Project::test(fs.clone(), [], cx).await;
4116 let project2 = Project::test(fs.clone(), [], cx).await;
4117
4118 let (multi_workspace, cx) =
4119 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project1.clone(), window, cx));
4120
4121 multi_workspace.update_in(cx, |mw, _, cx| {
4122 mw.set_random_database_id(cx);
4123 });
4124
4125 // Get a real DB id for workspace2 so the row actually exists.
4126 let workspace2_db_id = DB.next_id().await.unwrap();
4127
4128 multi_workspace.update_in(cx, |mw, window, cx| {
4129 let workspace = cx.new(|cx| crate::Workspace::test_new(project2.clone(), window, cx));
4130 workspace.update(cx, |ws: &mut crate::Workspace, _cx| {
4131 ws.set_database_id(workspace2_db_id)
4132 });
4133 mw.activate(workspace.clone(), cx);
4134 });
4135
4136 // Save a full workspace row to the DB directly.
4137 DB.save_workspace(SerializedWorkspace {
4138 id: workspace2_db_id,
4139 paths: PathList::new(&["/tmp/remove_test"]),
4140 location: SerializedWorkspaceLocation::Local,
4141 center_group: Default::default(),
4142 window_bounds: Default::default(),
4143 display: Default::default(),
4144 docks: Default::default(),
4145 centered_layout: false,
4146 session_id: Some("remove-test-session".to_owned()),
4147 breakpoints: Default::default(),
4148 window_id: Some(99),
4149 user_toolchains: Default::default(),
4150 })
4151 .await;
4152
4153 assert!(
4154 DB.workspace_for_id(workspace2_db_id).is_some(),
4155 "Workspace2 should exist in DB before removal"
4156 );
4157
4158 // Remove workspace at index 1 (the second workspace).
4159 multi_workspace.update_in(cx, |mw, window, cx| {
4160 mw.remove_workspace(1, window, cx);
4161 });
4162
4163 cx.run_until_parked();
4164
4165 // The row should be deleted, not just have session_id cleared.
4166 assert!(
4167 DB.workspace_for_id(workspace2_db_id).is_none(),
4168 "Removed workspace's DB row should be deleted entirely"
4169 );
4170 }
4171
4172 #[gpui::test]
4173 async fn test_remove_workspace_not_restored_as_zombie(cx: &mut gpui::TestAppContext) {
4174 use crate::multi_workspace::MultiWorkspace;
4175 use feature_flags::FeatureFlagAppExt;
4176 use gpui::AppContext as _;
4177 use project::Project;
4178
4179 crate::tests::init_test(cx);
4180
4181 cx.update(|cx| {
4182 cx.set_staff(true);
4183 cx.update_flags(true, vec!["agent-v2".to_string()]);
4184 });
4185
4186 let fs = fs::FakeFs::new(cx.executor());
4187 let dir1 = tempfile::TempDir::with_prefix("zombie_test1").unwrap();
4188 let dir2 = tempfile::TempDir::with_prefix("zombie_test2").unwrap();
4189 fs.insert_tree(dir1.path(), json!({})).await;
4190 fs.insert_tree(dir2.path(), json!({})).await;
4191
4192 let project1 = Project::test(fs.clone(), [], cx).await;
4193 let project2 = Project::test(fs.clone(), [], cx).await;
4194
4195 // Get real DB ids so the rows actually exist.
4196 let ws1_id = DB.next_id().await.unwrap();
4197 let ws2_id = DB.next_id().await.unwrap();
4198
4199 let (multi_workspace, cx) =
4200 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project1.clone(), window, cx));
4201
4202 multi_workspace.update_in(cx, |mw, _, cx| {
4203 mw.workspace().update(cx, |ws, _cx| {
4204 ws.set_database_id(ws1_id);
4205 });
4206 });
4207
4208 multi_workspace.update_in(cx, |mw, window, cx| {
4209 let workspace = cx.new(|cx| crate::Workspace::test_new(project2.clone(), window, cx));
4210 workspace.update(cx, |ws: &mut crate::Workspace, _cx| {
4211 ws.set_database_id(ws2_id)
4212 });
4213 mw.activate(workspace.clone(), cx);
4214 });
4215
4216 let session_id = "test-zombie-session";
4217 let window_id_val: u64 = 42;
4218
4219 DB.save_workspace(SerializedWorkspace {
4220 id: ws1_id,
4221 paths: PathList::new(&[dir1.path()]),
4222 location: SerializedWorkspaceLocation::Local,
4223 center_group: Default::default(),
4224 window_bounds: Default::default(),
4225 display: Default::default(),
4226 docks: Default::default(),
4227 centered_layout: false,
4228 session_id: Some(session_id.to_owned()),
4229 breakpoints: Default::default(),
4230 window_id: Some(window_id_val),
4231 user_toolchains: Default::default(),
4232 })
4233 .await;
4234
4235 DB.save_workspace(SerializedWorkspace {
4236 id: ws2_id,
4237 paths: PathList::new(&[dir2.path()]),
4238 location: SerializedWorkspaceLocation::Local,
4239 center_group: Default::default(),
4240 window_bounds: Default::default(),
4241 display: Default::default(),
4242 docks: Default::default(),
4243 centered_layout: false,
4244 session_id: Some(session_id.to_owned()),
4245 breakpoints: Default::default(),
4246 window_id: Some(window_id_val),
4247 user_toolchains: Default::default(),
4248 })
4249 .await;
4250
4251 // Remove workspace2 (index 1).
4252 multi_workspace.update_in(cx, |mw, window, cx| {
4253 mw.remove_workspace(1, window, cx);
4254 });
4255
4256 cx.run_until_parked();
4257
4258 // The removed workspace should NOT appear in session restoration.
4259 let locations = DB
4260 .last_session_workspace_locations(session_id, None, fs.as_ref())
4261 .await
4262 .unwrap();
4263
4264 let restored_ids: Vec<WorkspaceId> = locations.iter().map(|sw| sw.workspace_id).collect();
4265 assert!(
4266 !restored_ids.contains(&ws2_id),
4267 "Removed workspace should not appear in session restoration list. Found: {:?}",
4268 restored_ids
4269 );
4270 assert!(
4271 restored_ids.contains(&ws1_id),
4272 "Remaining workspace should still appear in session restoration list"
4273 );
4274 }
4275
4276 #[gpui::test]
4277 async fn test_pending_removal_tasks_drained_on_flush(cx: &mut gpui::TestAppContext) {
4278 use crate::multi_workspace::MultiWorkspace;
4279 use feature_flags::FeatureFlagAppExt;
4280 use gpui::AppContext as _;
4281 use project::Project;
4282
4283 crate::tests::init_test(cx);
4284
4285 cx.update(|cx| {
4286 cx.set_staff(true);
4287 cx.update_flags(true, vec!["agent-v2".to_string()]);
4288 });
4289
4290 let fs = fs::FakeFs::new(cx.executor());
4291 let project1 = Project::test(fs.clone(), [], cx).await;
4292 let project2 = Project::test(fs.clone(), [], cx).await;
4293
4294 // Get a real DB id for workspace2 so the row actually exists.
4295 let workspace2_db_id = DB.next_id().await.unwrap();
4296
4297 let (multi_workspace, cx) =
4298 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project1.clone(), window, cx));
4299
4300 multi_workspace.update_in(cx, |mw, _, cx| {
4301 mw.set_random_database_id(cx);
4302 });
4303
4304 multi_workspace.update_in(cx, |mw, window, cx| {
4305 let workspace = cx.new(|cx| crate::Workspace::test_new(project2.clone(), window, cx));
4306 workspace.update(cx, |ws: &mut crate::Workspace, _cx| {
4307 ws.set_database_id(workspace2_db_id)
4308 });
4309 mw.activate(workspace.clone(), cx);
4310 });
4311
4312 // Save a full workspace row to the DB directly and let it settle.
4313 DB.save_workspace(SerializedWorkspace {
4314 id: workspace2_db_id,
4315 paths: PathList::new(&["/tmp/pending_removal_test"]),
4316 location: SerializedWorkspaceLocation::Local,
4317 center_group: Default::default(),
4318 window_bounds: Default::default(),
4319 display: Default::default(),
4320 docks: Default::default(),
4321 centered_layout: false,
4322 session_id: Some("pending-removal-session".to_owned()),
4323 breakpoints: Default::default(),
4324 window_id: Some(88),
4325 user_toolchains: Default::default(),
4326 })
4327 .await;
4328 cx.run_until_parked();
4329
4330 // Remove workspace2 — this pushes a task to pending_removal_tasks.
4331 multi_workspace.update_in(cx, |mw, window, cx| {
4332 mw.remove_workspace(1, window, cx);
4333 });
4334
4335 // Simulate the quit handler pattern: collect flush tasks + pending
4336 // removal tasks and await them all.
4337 let all_tasks = multi_workspace.update_in(cx, |mw, window, cx| {
4338 let mut tasks: Vec<Task<()>> = mw
4339 .workspaces()
4340 .iter()
4341 .map(|workspace| {
4342 workspace.update(cx, |workspace, cx| {
4343 workspace.flush_serialization(window, cx)
4344 })
4345 })
4346 .collect();
4347 let mut removal_tasks = mw.take_pending_removal_tasks();
4348 // Note: removal_tasks may be empty if the background task already
4349 // completed (take_pending_removal_tasks filters out ready tasks).
4350 tasks.append(&mut removal_tasks);
4351 tasks.push(mw.flush_serialization());
4352 tasks
4353 });
4354 futures::future::join_all(all_tasks).await;
4355
4356 // After awaiting, the DB row should be deleted.
4357 assert!(
4358 DB.workspace_for_id(workspace2_db_id).is_none(),
4359 "Pending removal task should have deleted the workspace row when awaited"
4360 );
4361 }
4362
4363 #[gpui::test]
4364 async fn test_create_workspace_bounds_observer_uses_fresh_id(cx: &mut gpui::TestAppContext) {
4365 use crate::multi_workspace::MultiWorkspace;
4366 use feature_flags::FeatureFlagAppExt;
4367 use project::Project;
4368
4369 crate::tests::init_test(cx);
4370
4371 cx.update(|cx| {
4372 cx.set_staff(true);
4373 cx.update_flags(true, vec!["agent-v2".to_string()]);
4374 });
4375
4376 let fs = fs::FakeFs::new(cx.executor());
4377 let project = Project::test(fs.clone(), [], cx).await;
4378
4379 let (multi_workspace, cx) =
4380 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
4381
4382 multi_workspace.update_in(cx, |mw, _, cx| {
4383 mw.set_random_database_id(cx);
4384 });
4385
4386 multi_workspace.update_in(cx, |mw, window, cx| {
4387 mw.create_workspace(window, cx);
4388 });
4389
4390 cx.run_until_parked();
4391
4392 let new_workspace_db_id =
4393 multi_workspace.read_with(cx, |mw, cx| mw.workspace().read(cx).database_id());
4394 assert!(
4395 new_workspace_db_id.is_some(),
4396 "After run_until_parked, the workspace should have a database_id"
4397 );
4398
4399 let workspace_id = new_workspace_db_id.unwrap();
4400
4401 assert!(
4402 DB.workspace_for_id(workspace_id).is_some(),
4403 "The workspace row should exist in the DB"
4404 );
4405
4406 cx.simulate_resize(gpui::size(px(1024.0), px(768.0)));
4407
4408 // Advance the clock past the 100ms debounce timer so the bounds
4409 // observer task fires
4410 cx.executor().advance_clock(Duration::from_millis(200));
4411 cx.run_until_parked();
4412
4413 let serialized = DB
4414 .workspace_for_id(workspace_id)
4415 .expect("workspace row should still exist");
4416 assert!(
4417 serialized.window_bounds.is_some(),
4418 "The bounds observer should write bounds for the workspace's real DB ID, \
4419 even when the workspace was created via create_workspace (where the ID \
4420 is assigned asynchronously after construction)."
4421 );
4422 }
4423
4424 #[gpui::test]
4425 async fn test_flush_serialization_writes_bounds(cx: &mut gpui::TestAppContext) {
4426 use crate::multi_workspace::MultiWorkspace;
4427 use feature_flags::FeatureFlagAppExt;
4428 use project::Project;
4429
4430 crate::tests::init_test(cx);
4431
4432 cx.update(|cx| {
4433 cx.set_staff(true);
4434 cx.update_flags(true, vec!["agent-v2".to_string()]);
4435 });
4436
4437 let fs = fs::FakeFs::new(cx.executor());
4438 let dir = tempfile::TempDir::with_prefix("flush_bounds_test").unwrap();
4439 fs.insert_tree(dir.path(), json!({})).await;
4440
4441 let project = Project::test(fs.clone(), [dir.path()], cx).await;
4442
4443 let (multi_workspace, cx) =
4444 cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
4445
4446 let workspace_id = DB.next_id().await.unwrap();
4447 multi_workspace.update_in(cx, |mw, _, cx| {
4448 mw.workspace().update(cx, |ws, _cx| {
4449 ws.set_database_id(workspace_id);
4450 });
4451 });
4452
4453 let task = multi_workspace.update_in(cx, |mw, window, cx| {
4454 mw.workspace()
4455 .update(cx, |ws, cx| ws.flush_serialization(window, cx))
4456 });
4457 task.await;
4458
4459 let after = DB
4460 .workspace_for_id(workspace_id)
4461 .expect("workspace row should exist after flush_serialization");
4462 assert!(
4463 !after.paths.is_empty(),
4464 "flush_serialization should have written paths via save_workspace"
4465 );
4466 assert!(
4467 after.window_bounds.is_some(),
4468 "flush_serialization should ensure window bounds are persisted to the DB \
4469 before the process exits."
4470 );
4471 }
4472}