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