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