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_breakpoints() {
2360 zlog::init_test();
2361
2362 let db = WorkspaceDb::open_test_db("test_breakpoints").await;
2363 let id = db.next_id().await.unwrap();
2364
2365 let path = Path::new("/tmp/test.rs");
2366
2367 let breakpoint = Breakpoint {
2368 position: 123,
2369 message: None,
2370 state: BreakpointState::Enabled,
2371 condition: None,
2372 hit_condition: None,
2373 };
2374
2375 let log_breakpoint = Breakpoint {
2376 position: 456,
2377 message: Some("Test log message".into()),
2378 state: BreakpointState::Enabled,
2379 condition: None,
2380 hit_condition: None,
2381 };
2382
2383 let disable_breakpoint = Breakpoint {
2384 position: 578,
2385 message: None,
2386 state: BreakpointState::Disabled,
2387 condition: None,
2388 hit_condition: None,
2389 };
2390
2391 let condition_breakpoint = Breakpoint {
2392 position: 789,
2393 message: None,
2394 state: BreakpointState::Enabled,
2395 condition: Some("x > 5".into()),
2396 hit_condition: None,
2397 };
2398
2399 let hit_condition_breakpoint = Breakpoint {
2400 position: 999,
2401 message: None,
2402 state: BreakpointState::Enabled,
2403 condition: None,
2404 hit_condition: Some(">= 3".into()),
2405 };
2406
2407 let workspace = SerializedWorkspace {
2408 id,
2409 paths: PathList::new(&["/tmp"]),
2410 location: SerializedWorkspaceLocation::Local,
2411 center_group: Default::default(),
2412 window_bounds: Default::default(),
2413 display: Default::default(),
2414 docks: Default::default(),
2415 centered_layout: false,
2416 breakpoints: {
2417 let mut map = collections::BTreeMap::default();
2418 map.insert(
2419 Arc::from(path),
2420 vec![
2421 SourceBreakpoint {
2422 row: breakpoint.position,
2423 path: Arc::from(path),
2424 message: breakpoint.message.clone(),
2425 state: breakpoint.state,
2426 condition: breakpoint.condition.clone(),
2427 hit_condition: breakpoint.hit_condition.clone(),
2428 },
2429 SourceBreakpoint {
2430 row: log_breakpoint.position,
2431 path: Arc::from(path),
2432 message: log_breakpoint.message.clone(),
2433 state: log_breakpoint.state,
2434 condition: log_breakpoint.condition.clone(),
2435 hit_condition: log_breakpoint.hit_condition.clone(),
2436 },
2437 SourceBreakpoint {
2438 row: disable_breakpoint.position,
2439 path: Arc::from(path),
2440 message: disable_breakpoint.message.clone(),
2441 state: disable_breakpoint.state,
2442 condition: disable_breakpoint.condition.clone(),
2443 hit_condition: disable_breakpoint.hit_condition.clone(),
2444 },
2445 SourceBreakpoint {
2446 row: condition_breakpoint.position,
2447 path: Arc::from(path),
2448 message: condition_breakpoint.message.clone(),
2449 state: condition_breakpoint.state,
2450 condition: condition_breakpoint.condition.clone(),
2451 hit_condition: condition_breakpoint.hit_condition.clone(),
2452 },
2453 SourceBreakpoint {
2454 row: hit_condition_breakpoint.position,
2455 path: Arc::from(path),
2456 message: hit_condition_breakpoint.message.clone(),
2457 state: hit_condition_breakpoint.state,
2458 condition: hit_condition_breakpoint.condition.clone(),
2459 hit_condition: hit_condition_breakpoint.hit_condition.clone(),
2460 },
2461 ],
2462 );
2463 map
2464 },
2465 session_id: None,
2466 window_id: None,
2467 user_toolchains: Default::default(),
2468 };
2469
2470 db.save_workspace(workspace.clone()).await;
2471
2472 let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
2473 let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(path)).unwrap();
2474
2475 assert_eq!(loaded_breakpoints.len(), 5);
2476
2477 // normal breakpoint
2478 assert_eq!(loaded_breakpoints[0].row, breakpoint.position);
2479 assert_eq!(loaded_breakpoints[0].message, breakpoint.message);
2480 assert_eq!(loaded_breakpoints[0].condition, breakpoint.condition);
2481 assert_eq!(
2482 loaded_breakpoints[0].hit_condition,
2483 breakpoint.hit_condition
2484 );
2485 assert_eq!(loaded_breakpoints[0].state, breakpoint.state);
2486 assert_eq!(loaded_breakpoints[0].path, Arc::from(path));
2487
2488 // enabled breakpoint
2489 assert_eq!(loaded_breakpoints[1].row, log_breakpoint.position);
2490 assert_eq!(loaded_breakpoints[1].message, log_breakpoint.message);
2491 assert_eq!(loaded_breakpoints[1].condition, log_breakpoint.condition);
2492 assert_eq!(
2493 loaded_breakpoints[1].hit_condition,
2494 log_breakpoint.hit_condition
2495 );
2496 assert_eq!(loaded_breakpoints[1].state, log_breakpoint.state);
2497 assert_eq!(loaded_breakpoints[1].path, Arc::from(path));
2498
2499 // disable breakpoint
2500 assert_eq!(loaded_breakpoints[2].row, disable_breakpoint.position);
2501 assert_eq!(loaded_breakpoints[2].message, disable_breakpoint.message);
2502 assert_eq!(
2503 loaded_breakpoints[2].condition,
2504 disable_breakpoint.condition
2505 );
2506 assert_eq!(
2507 loaded_breakpoints[2].hit_condition,
2508 disable_breakpoint.hit_condition
2509 );
2510 assert_eq!(loaded_breakpoints[2].state, disable_breakpoint.state);
2511 assert_eq!(loaded_breakpoints[2].path, Arc::from(path));
2512
2513 // condition breakpoint
2514 assert_eq!(loaded_breakpoints[3].row, condition_breakpoint.position);
2515 assert_eq!(loaded_breakpoints[3].message, condition_breakpoint.message);
2516 assert_eq!(
2517 loaded_breakpoints[3].condition,
2518 condition_breakpoint.condition
2519 );
2520 assert_eq!(
2521 loaded_breakpoints[3].hit_condition,
2522 condition_breakpoint.hit_condition
2523 );
2524 assert_eq!(loaded_breakpoints[3].state, condition_breakpoint.state);
2525 assert_eq!(loaded_breakpoints[3].path, Arc::from(path));
2526
2527 // hit condition breakpoint
2528 assert_eq!(loaded_breakpoints[4].row, hit_condition_breakpoint.position);
2529 assert_eq!(
2530 loaded_breakpoints[4].message,
2531 hit_condition_breakpoint.message
2532 );
2533 assert_eq!(
2534 loaded_breakpoints[4].condition,
2535 hit_condition_breakpoint.condition
2536 );
2537 assert_eq!(
2538 loaded_breakpoints[4].hit_condition,
2539 hit_condition_breakpoint.hit_condition
2540 );
2541 assert_eq!(loaded_breakpoints[4].state, hit_condition_breakpoint.state);
2542 assert_eq!(loaded_breakpoints[4].path, Arc::from(path));
2543 }
2544
2545 #[gpui::test]
2546 async fn test_remove_last_breakpoint() {
2547 zlog::init_test();
2548
2549 let db = WorkspaceDb::open_test_db("test_remove_last_breakpoint").await;
2550 let id = db.next_id().await.unwrap();
2551
2552 let singular_path = Path::new("/tmp/test_remove_last_breakpoint.rs");
2553
2554 let breakpoint_to_remove = Breakpoint {
2555 position: 100,
2556 message: None,
2557 state: BreakpointState::Enabled,
2558 condition: None,
2559 hit_condition: None,
2560 };
2561
2562 let workspace = SerializedWorkspace {
2563 id,
2564 paths: PathList::new(&["/tmp"]),
2565 location: SerializedWorkspaceLocation::Local,
2566 center_group: Default::default(),
2567 window_bounds: Default::default(),
2568 display: Default::default(),
2569 docks: Default::default(),
2570 centered_layout: false,
2571 breakpoints: {
2572 let mut map = collections::BTreeMap::default();
2573 map.insert(
2574 Arc::from(singular_path),
2575 vec![SourceBreakpoint {
2576 row: breakpoint_to_remove.position,
2577 path: Arc::from(singular_path),
2578 message: None,
2579 state: BreakpointState::Enabled,
2580 condition: None,
2581 hit_condition: None,
2582 }],
2583 );
2584 map
2585 },
2586 session_id: None,
2587 window_id: None,
2588 user_toolchains: Default::default(),
2589 };
2590
2591 db.save_workspace(workspace.clone()).await;
2592
2593 let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
2594 let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(singular_path)).unwrap();
2595
2596 assert_eq!(loaded_breakpoints.len(), 1);
2597 assert_eq!(loaded_breakpoints[0].row, breakpoint_to_remove.position);
2598 assert_eq!(loaded_breakpoints[0].message, breakpoint_to_remove.message);
2599 assert_eq!(
2600 loaded_breakpoints[0].condition,
2601 breakpoint_to_remove.condition
2602 );
2603 assert_eq!(
2604 loaded_breakpoints[0].hit_condition,
2605 breakpoint_to_remove.hit_condition
2606 );
2607 assert_eq!(loaded_breakpoints[0].state, breakpoint_to_remove.state);
2608 assert_eq!(loaded_breakpoints[0].path, Arc::from(singular_path));
2609
2610 let workspace_without_breakpoint = SerializedWorkspace {
2611 id,
2612 paths: PathList::new(&["/tmp"]),
2613 location: SerializedWorkspaceLocation::Local,
2614 center_group: Default::default(),
2615 window_bounds: Default::default(),
2616 display: Default::default(),
2617 docks: Default::default(),
2618 centered_layout: false,
2619 breakpoints: collections::BTreeMap::default(),
2620 session_id: None,
2621 window_id: None,
2622 user_toolchains: Default::default(),
2623 };
2624
2625 db.save_workspace(workspace_without_breakpoint.clone())
2626 .await;
2627
2628 let loaded_after_remove = db.workspace_for_roots(&["/tmp"]).unwrap();
2629 let empty_breakpoints = loaded_after_remove
2630 .breakpoints
2631 .get(&Arc::from(singular_path));
2632
2633 assert!(empty_breakpoints.is_none());
2634 }
2635
2636 #[gpui::test]
2637 async fn test_next_id_stability() {
2638 zlog::init_test();
2639
2640 let db = WorkspaceDb::open_test_db("test_next_id_stability").await;
2641
2642 db.write(|conn| {
2643 conn.migrate(
2644 "test_table",
2645 &[sql!(
2646 CREATE TABLE test_table(
2647 text TEXT,
2648 workspace_id INTEGER,
2649 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
2650 ON DELETE CASCADE
2651 ) STRICT;
2652 )],
2653 |_, _, _| false,
2654 )
2655 .unwrap();
2656 })
2657 .await;
2658
2659 let id = db.next_id().await.unwrap();
2660 // Assert the empty row got inserted
2661 assert_eq!(
2662 Some(id),
2663 db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
2664 SELECT workspace_id FROM workspaces WHERE workspace_id = ?
2665 ))
2666 .unwrap()(id)
2667 .unwrap()
2668 );
2669
2670 db.write(move |conn| {
2671 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2672 .unwrap()(("test-text-1", id))
2673 .unwrap()
2674 })
2675 .await;
2676
2677 let test_text_1 = db
2678 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2679 .unwrap()(1)
2680 .unwrap()
2681 .unwrap();
2682 assert_eq!(test_text_1, "test-text-1");
2683 }
2684
2685 #[gpui::test]
2686 async fn test_workspace_id_stability() {
2687 zlog::init_test();
2688
2689 let db = WorkspaceDb::open_test_db("test_workspace_id_stability").await;
2690
2691 db.write(|conn| {
2692 conn.migrate(
2693 "test_table",
2694 &[sql!(
2695 CREATE TABLE test_table(
2696 text TEXT,
2697 workspace_id INTEGER,
2698 FOREIGN KEY(workspace_id)
2699 REFERENCES workspaces(workspace_id)
2700 ON DELETE CASCADE
2701 ) STRICT;)],
2702 |_, _, _| false,
2703 )
2704 })
2705 .await
2706 .unwrap();
2707
2708 let mut workspace_1 = SerializedWorkspace {
2709 id: WorkspaceId(1),
2710 paths: PathList::new(&["/tmp", "/tmp2"]),
2711 location: SerializedWorkspaceLocation::Local,
2712 center_group: Default::default(),
2713 window_bounds: Default::default(),
2714 display: Default::default(),
2715 docks: Default::default(),
2716 centered_layout: false,
2717 breakpoints: Default::default(),
2718 session_id: None,
2719 window_id: None,
2720 user_toolchains: Default::default(),
2721 };
2722
2723 let workspace_2 = SerializedWorkspace {
2724 id: WorkspaceId(2),
2725 paths: PathList::new(&["/tmp"]),
2726 location: SerializedWorkspaceLocation::Local,
2727 center_group: Default::default(),
2728 window_bounds: Default::default(),
2729 display: Default::default(),
2730 docks: Default::default(),
2731 centered_layout: false,
2732 breakpoints: Default::default(),
2733 session_id: None,
2734 window_id: None,
2735 user_toolchains: Default::default(),
2736 };
2737
2738 db.save_workspace(workspace_1.clone()).await;
2739
2740 db.write(|conn| {
2741 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2742 .unwrap()(("test-text-1", 1))
2743 .unwrap();
2744 })
2745 .await;
2746
2747 db.save_workspace(workspace_2.clone()).await;
2748
2749 db.write(|conn| {
2750 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2751 .unwrap()(("test-text-2", 2))
2752 .unwrap();
2753 })
2754 .await;
2755
2756 workspace_1.paths = PathList::new(&["/tmp", "/tmp3"]);
2757 db.save_workspace(workspace_1.clone()).await;
2758 db.save_workspace(workspace_1).await;
2759 db.save_workspace(workspace_2).await;
2760
2761 let test_text_2 = db
2762 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2763 .unwrap()(2)
2764 .unwrap()
2765 .unwrap();
2766 assert_eq!(test_text_2, "test-text-2");
2767
2768 let test_text_1 = db
2769 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2770 .unwrap()(1)
2771 .unwrap()
2772 .unwrap();
2773 assert_eq!(test_text_1, "test-text-1");
2774 }
2775
2776 fn group(axis: Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
2777 SerializedPaneGroup::Group {
2778 axis: SerializedAxis(axis),
2779 flexes: None,
2780 children,
2781 }
2782 }
2783
2784 #[gpui::test]
2785 async fn test_full_workspace_serialization() {
2786 zlog::init_test();
2787
2788 let db = WorkspaceDb::open_test_db("test_full_workspace_serialization").await;
2789
2790 // -----------------
2791 // | 1,2 | 5,6 |
2792 // | - - - | |
2793 // | 3,4 | |
2794 // -----------------
2795 let center_group = group(
2796 Axis::Horizontal,
2797 vec![
2798 group(
2799 Axis::Vertical,
2800 vec![
2801 SerializedPaneGroup::Pane(SerializedPane::new(
2802 vec![
2803 SerializedItem::new("Terminal", 5, false, false),
2804 SerializedItem::new("Terminal", 6, true, false),
2805 ],
2806 false,
2807 0,
2808 )),
2809 SerializedPaneGroup::Pane(SerializedPane::new(
2810 vec![
2811 SerializedItem::new("Terminal", 7, true, false),
2812 SerializedItem::new("Terminal", 8, false, false),
2813 ],
2814 false,
2815 0,
2816 )),
2817 ],
2818 ),
2819 SerializedPaneGroup::Pane(SerializedPane::new(
2820 vec![
2821 SerializedItem::new("Terminal", 9, false, false),
2822 SerializedItem::new("Terminal", 10, true, false),
2823 ],
2824 false,
2825 0,
2826 )),
2827 ],
2828 );
2829
2830 let workspace = SerializedWorkspace {
2831 id: WorkspaceId(5),
2832 paths: PathList::new(&["/tmp", "/tmp2"]),
2833 location: SerializedWorkspaceLocation::Local,
2834 center_group,
2835 window_bounds: Default::default(),
2836 breakpoints: Default::default(),
2837 display: Default::default(),
2838 docks: Default::default(),
2839 centered_layout: false,
2840 session_id: None,
2841 window_id: Some(999),
2842 user_toolchains: Default::default(),
2843 };
2844
2845 db.save_workspace(workspace.clone()).await;
2846
2847 let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
2848 assert_eq!(workspace, round_trip_workspace.unwrap());
2849
2850 // Test guaranteed duplicate IDs
2851 db.save_workspace(workspace.clone()).await;
2852 db.save_workspace(workspace.clone()).await;
2853
2854 let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
2855 assert_eq!(workspace, round_trip_workspace.unwrap());
2856 }
2857
2858 #[gpui::test]
2859 async fn test_workspace_assignment() {
2860 zlog::init_test();
2861
2862 let db = WorkspaceDb::open_test_db("test_basic_functionality").await;
2863
2864 let workspace_1 = SerializedWorkspace {
2865 id: WorkspaceId(1),
2866 paths: PathList::new(&["/tmp", "/tmp2"]),
2867 location: SerializedWorkspaceLocation::Local,
2868 center_group: Default::default(),
2869 window_bounds: Default::default(),
2870 breakpoints: Default::default(),
2871 display: Default::default(),
2872 docks: Default::default(),
2873 centered_layout: false,
2874 session_id: None,
2875 window_id: Some(1),
2876 user_toolchains: Default::default(),
2877 };
2878
2879 let mut workspace_2 = SerializedWorkspace {
2880 id: WorkspaceId(2),
2881 paths: PathList::new(&["/tmp"]),
2882 location: SerializedWorkspaceLocation::Local,
2883 center_group: Default::default(),
2884 window_bounds: Default::default(),
2885 display: Default::default(),
2886 docks: Default::default(),
2887 centered_layout: false,
2888 breakpoints: Default::default(),
2889 session_id: None,
2890 window_id: Some(2),
2891 user_toolchains: Default::default(),
2892 };
2893
2894 db.save_workspace(workspace_1.clone()).await;
2895 db.save_workspace(workspace_2.clone()).await;
2896
2897 // Test that paths are treated as a set
2898 assert_eq!(
2899 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2900 workspace_1
2901 );
2902 assert_eq!(
2903 db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
2904 workspace_1
2905 );
2906
2907 // Make sure that other keys work
2908 assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
2909 assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
2910
2911 // Test 'mutate' case of updating a pre-existing id
2912 workspace_2.paths = PathList::new(&["/tmp", "/tmp2"]);
2913
2914 db.save_workspace(workspace_2.clone()).await;
2915 assert_eq!(
2916 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2917 workspace_2
2918 );
2919
2920 // Test other mechanism for mutating
2921 let mut workspace_3 = SerializedWorkspace {
2922 id: WorkspaceId(3),
2923 paths: PathList::new(&["/tmp2", "/tmp"]),
2924 location: SerializedWorkspaceLocation::Local,
2925 center_group: Default::default(),
2926 window_bounds: Default::default(),
2927 breakpoints: Default::default(),
2928 display: Default::default(),
2929 docks: Default::default(),
2930 centered_layout: false,
2931 session_id: None,
2932 window_id: Some(3),
2933 user_toolchains: Default::default(),
2934 };
2935
2936 db.save_workspace(workspace_3.clone()).await;
2937 assert_eq!(
2938 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2939 workspace_3
2940 );
2941
2942 // Make sure that updating paths differently also works
2943 workspace_3.paths = PathList::new(&["/tmp3", "/tmp4", "/tmp2"]);
2944 db.save_workspace(workspace_3.clone()).await;
2945 assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
2946 assert_eq!(
2947 db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
2948 .unwrap(),
2949 workspace_3
2950 );
2951 }
2952
2953 #[gpui::test]
2954 async fn test_session_workspaces() {
2955 zlog::init_test();
2956
2957 let db = WorkspaceDb::open_test_db("test_serializing_workspaces_session_id").await;
2958
2959 let workspace_1 = SerializedWorkspace {
2960 id: WorkspaceId(1),
2961 paths: PathList::new(&["/tmp1"]),
2962 location: SerializedWorkspaceLocation::Local,
2963 center_group: Default::default(),
2964 window_bounds: Default::default(),
2965 display: Default::default(),
2966 docks: Default::default(),
2967 centered_layout: false,
2968 breakpoints: Default::default(),
2969 session_id: Some("session-id-1".to_owned()),
2970 window_id: Some(10),
2971 user_toolchains: Default::default(),
2972 };
2973
2974 let workspace_2 = SerializedWorkspace {
2975 id: WorkspaceId(2),
2976 paths: PathList::new(&["/tmp2"]),
2977 location: SerializedWorkspaceLocation::Local,
2978 center_group: Default::default(),
2979 window_bounds: Default::default(),
2980 display: Default::default(),
2981 docks: Default::default(),
2982 centered_layout: false,
2983 breakpoints: Default::default(),
2984 session_id: Some("session-id-1".to_owned()),
2985 window_id: Some(20),
2986 user_toolchains: Default::default(),
2987 };
2988
2989 let workspace_3 = SerializedWorkspace {
2990 id: WorkspaceId(3),
2991 paths: PathList::new(&["/tmp3"]),
2992 location: SerializedWorkspaceLocation::Local,
2993 center_group: Default::default(),
2994 window_bounds: Default::default(),
2995 display: Default::default(),
2996 docks: Default::default(),
2997 centered_layout: false,
2998 breakpoints: Default::default(),
2999 session_id: Some("session-id-2".to_owned()),
3000 window_id: Some(30),
3001 user_toolchains: Default::default(),
3002 };
3003
3004 let workspace_4 = SerializedWorkspace {
3005 id: WorkspaceId(4),
3006 paths: PathList::new(&["/tmp4"]),
3007 location: SerializedWorkspaceLocation::Local,
3008 center_group: Default::default(),
3009 window_bounds: Default::default(),
3010 display: Default::default(),
3011 docks: Default::default(),
3012 centered_layout: false,
3013 breakpoints: Default::default(),
3014 session_id: None,
3015 window_id: None,
3016 user_toolchains: Default::default(),
3017 };
3018
3019 let connection_id = db
3020 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3021 host: "my-host".into(),
3022 port: Some(1234),
3023 ..Default::default()
3024 }))
3025 .await
3026 .unwrap();
3027
3028 let workspace_5 = SerializedWorkspace {
3029 id: WorkspaceId(5),
3030 paths: PathList::default(),
3031 location: SerializedWorkspaceLocation::Remote(
3032 db.remote_connection(connection_id).unwrap(),
3033 ),
3034 center_group: Default::default(),
3035 window_bounds: Default::default(),
3036 display: Default::default(),
3037 docks: Default::default(),
3038 centered_layout: false,
3039 breakpoints: Default::default(),
3040 session_id: Some("session-id-2".to_owned()),
3041 window_id: Some(50),
3042 user_toolchains: Default::default(),
3043 };
3044
3045 let workspace_6 = SerializedWorkspace {
3046 id: WorkspaceId(6),
3047 paths: PathList::new(&["/tmp6c", "/tmp6b", "/tmp6a"]),
3048 location: SerializedWorkspaceLocation::Local,
3049 center_group: Default::default(),
3050 window_bounds: Default::default(),
3051 breakpoints: Default::default(),
3052 display: Default::default(),
3053 docks: Default::default(),
3054 centered_layout: false,
3055 session_id: Some("session-id-3".to_owned()),
3056 window_id: Some(60),
3057 user_toolchains: Default::default(),
3058 };
3059
3060 db.save_workspace(workspace_1.clone()).await;
3061 thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
3062 db.save_workspace(workspace_2.clone()).await;
3063 db.save_workspace(workspace_3.clone()).await;
3064 thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
3065 db.save_workspace(workspace_4.clone()).await;
3066 db.save_workspace(workspace_5.clone()).await;
3067 db.save_workspace(workspace_6.clone()).await;
3068
3069 let locations = db.session_workspaces("session-id-1".to_owned()).unwrap();
3070 assert_eq!(locations.len(), 2);
3071 assert_eq!(locations[0].0, WorkspaceId(2));
3072 assert_eq!(locations[0].1, PathList::new(&["/tmp2"]));
3073 assert_eq!(locations[0].2, Some(20));
3074 assert_eq!(locations[1].0, WorkspaceId(1));
3075 assert_eq!(locations[1].1, PathList::new(&["/tmp1"]));
3076 assert_eq!(locations[1].2, Some(10));
3077
3078 let locations = db.session_workspaces("session-id-2".to_owned()).unwrap();
3079 assert_eq!(locations.len(), 2);
3080 assert_eq!(locations[0].0, WorkspaceId(5));
3081 assert_eq!(locations[0].1, PathList::default());
3082 assert_eq!(locations[0].2, Some(50));
3083 assert_eq!(locations[0].3, Some(connection_id));
3084 assert_eq!(locations[1].0, WorkspaceId(3));
3085 assert_eq!(locations[1].1, PathList::new(&["/tmp3"]));
3086 assert_eq!(locations[1].2, Some(30));
3087
3088 let locations = db.session_workspaces("session-id-3".to_owned()).unwrap();
3089 assert_eq!(locations.len(), 1);
3090 assert_eq!(locations[0].0, WorkspaceId(6));
3091 assert_eq!(
3092 locations[0].1,
3093 PathList::new(&["/tmp6c", "/tmp6b", "/tmp6a"]),
3094 );
3095 assert_eq!(locations[0].2, Some(60));
3096 }
3097
3098 fn default_workspace<P: AsRef<Path>>(
3099 paths: &[P],
3100 center_group: &SerializedPaneGroup,
3101 ) -> SerializedWorkspace {
3102 SerializedWorkspace {
3103 id: WorkspaceId(4),
3104 paths: PathList::new(paths),
3105 location: SerializedWorkspaceLocation::Local,
3106 center_group: center_group.clone(),
3107 window_bounds: Default::default(),
3108 display: Default::default(),
3109 docks: Default::default(),
3110 breakpoints: Default::default(),
3111 centered_layout: false,
3112 session_id: None,
3113 window_id: None,
3114 user_toolchains: Default::default(),
3115 }
3116 }
3117
3118 #[gpui::test]
3119 async fn test_last_session_workspace_locations(cx: &mut gpui::TestAppContext) {
3120 let dir1 = tempfile::TempDir::with_prefix("dir1").unwrap();
3121 let dir2 = tempfile::TempDir::with_prefix("dir2").unwrap();
3122 let dir3 = tempfile::TempDir::with_prefix("dir3").unwrap();
3123 let dir4 = tempfile::TempDir::with_prefix("dir4").unwrap();
3124
3125 let fs = fs::FakeFs::new(cx.executor());
3126 fs.insert_tree(dir1.path(), json!({})).await;
3127 fs.insert_tree(dir2.path(), json!({})).await;
3128 fs.insert_tree(dir3.path(), json!({})).await;
3129 fs.insert_tree(dir4.path(), json!({})).await;
3130
3131 let db =
3132 WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces").await;
3133
3134 let workspaces = [
3135 (1, vec![dir1.path()], 9),
3136 (2, vec![dir2.path()], 5),
3137 (3, vec![dir3.path()], 8),
3138 (4, vec![dir4.path()], 2),
3139 (5, vec![dir1.path(), dir2.path(), dir3.path()], 3),
3140 (6, vec![dir4.path(), dir3.path(), dir2.path()], 4),
3141 ]
3142 .into_iter()
3143 .map(|(id, paths, window_id)| SerializedWorkspace {
3144 id: WorkspaceId(id),
3145 paths: PathList::new(paths.as_slice()),
3146 location: SerializedWorkspaceLocation::Local,
3147 center_group: Default::default(),
3148 window_bounds: Default::default(),
3149 display: Default::default(),
3150 docks: Default::default(),
3151 centered_layout: false,
3152 session_id: Some("one-session".to_owned()),
3153 breakpoints: Default::default(),
3154 window_id: Some(window_id),
3155 user_toolchains: Default::default(),
3156 })
3157 .collect::<Vec<_>>();
3158
3159 for workspace in workspaces.iter() {
3160 db.save_workspace(workspace.clone()).await;
3161 }
3162
3163 let stack = Some(Vec::from([
3164 WindowId::from(2), // Top
3165 WindowId::from(8),
3166 WindowId::from(5),
3167 WindowId::from(9),
3168 WindowId::from(3),
3169 WindowId::from(4), // Bottom
3170 ]));
3171
3172 let locations = db
3173 .last_session_workspace_locations("one-session", stack, fs.as_ref())
3174 .await
3175 .unwrap();
3176 assert_eq!(
3177 locations,
3178 [
3179 SessionWorkspace {
3180 workspace_id: WorkspaceId(4),
3181 location: SerializedWorkspaceLocation::Local,
3182 paths: PathList::new(&[dir4.path()]),
3183 window_id: Some(WindowId::from(2u64)),
3184 },
3185 SessionWorkspace {
3186 workspace_id: WorkspaceId(3),
3187 location: SerializedWorkspaceLocation::Local,
3188 paths: PathList::new(&[dir3.path()]),
3189 window_id: Some(WindowId::from(8u64)),
3190 },
3191 SessionWorkspace {
3192 workspace_id: WorkspaceId(2),
3193 location: SerializedWorkspaceLocation::Local,
3194 paths: PathList::new(&[dir2.path()]),
3195 window_id: Some(WindowId::from(5u64)),
3196 },
3197 SessionWorkspace {
3198 workspace_id: WorkspaceId(1),
3199 location: SerializedWorkspaceLocation::Local,
3200 paths: PathList::new(&[dir1.path()]),
3201 window_id: Some(WindowId::from(9u64)),
3202 },
3203 SessionWorkspace {
3204 workspace_id: WorkspaceId(5),
3205 location: SerializedWorkspaceLocation::Local,
3206 paths: PathList::new(&[dir1.path(), dir2.path(), dir3.path()]),
3207 window_id: Some(WindowId::from(3u64)),
3208 },
3209 SessionWorkspace {
3210 workspace_id: WorkspaceId(6),
3211 location: SerializedWorkspaceLocation::Local,
3212 paths: PathList::new(&[dir4.path(), dir3.path(), dir2.path()]),
3213 window_id: Some(WindowId::from(4u64)),
3214 },
3215 ]
3216 );
3217 }
3218
3219 #[gpui::test]
3220 async fn test_last_session_workspace_locations_remote(cx: &mut gpui::TestAppContext) {
3221 let fs = fs::FakeFs::new(cx.executor());
3222 let db =
3223 WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces_remote")
3224 .await;
3225
3226 let remote_connections = [
3227 ("host-1", "my-user-1"),
3228 ("host-2", "my-user-2"),
3229 ("host-3", "my-user-3"),
3230 ("host-4", "my-user-4"),
3231 ]
3232 .into_iter()
3233 .map(|(host, user)| async {
3234 let options = RemoteConnectionOptions::Ssh(SshConnectionOptions {
3235 host: host.into(),
3236 username: Some(user.to_string()),
3237 ..Default::default()
3238 });
3239 db.get_or_create_remote_connection(options.clone())
3240 .await
3241 .unwrap();
3242 options
3243 })
3244 .collect::<Vec<_>>();
3245
3246 let remote_connections = futures::future::join_all(remote_connections).await;
3247
3248 let workspaces = [
3249 (1, remote_connections[0].clone(), 9),
3250 (2, remote_connections[1].clone(), 5),
3251 (3, remote_connections[2].clone(), 8),
3252 (4, remote_connections[3].clone(), 2),
3253 ]
3254 .into_iter()
3255 .map(|(id, remote_connection, window_id)| SerializedWorkspace {
3256 id: WorkspaceId(id),
3257 paths: PathList::default(),
3258 location: SerializedWorkspaceLocation::Remote(remote_connection),
3259 center_group: Default::default(),
3260 window_bounds: Default::default(),
3261 display: Default::default(),
3262 docks: Default::default(),
3263 centered_layout: false,
3264 session_id: Some("one-session".to_owned()),
3265 breakpoints: Default::default(),
3266 window_id: Some(window_id),
3267 user_toolchains: Default::default(),
3268 })
3269 .collect::<Vec<_>>();
3270
3271 for workspace in workspaces.iter() {
3272 db.save_workspace(workspace.clone()).await;
3273 }
3274
3275 let stack = Some(Vec::from([
3276 WindowId::from(2), // Top
3277 WindowId::from(8),
3278 WindowId::from(5),
3279 WindowId::from(9), // Bottom
3280 ]));
3281
3282 let have = db
3283 .last_session_workspace_locations("one-session", stack, fs.as_ref())
3284 .await
3285 .unwrap();
3286 assert_eq!(have.len(), 4);
3287 assert_eq!(
3288 have[0],
3289 SessionWorkspace {
3290 workspace_id: WorkspaceId(4),
3291 location: SerializedWorkspaceLocation::Remote(remote_connections[3].clone()),
3292 paths: PathList::default(),
3293 window_id: Some(WindowId::from(2u64)),
3294 }
3295 );
3296 assert_eq!(
3297 have[1],
3298 SessionWorkspace {
3299 workspace_id: WorkspaceId(3),
3300 location: SerializedWorkspaceLocation::Remote(remote_connections[2].clone()),
3301 paths: PathList::default(),
3302 window_id: Some(WindowId::from(8u64)),
3303 }
3304 );
3305 assert_eq!(
3306 have[2],
3307 SessionWorkspace {
3308 workspace_id: WorkspaceId(2),
3309 location: SerializedWorkspaceLocation::Remote(remote_connections[1].clone()),
3310 paths: PathList::default(),
3311 window_id: Some(WindowId::from(5u64)),
3312 }
3313 );
3314 assert_eq!(
3315 have[3],
3316 SessionWorkspace {
3317 workspace_id: WorkspaceId(1),
3318 location: SerializedWorkspaceLocation::Remote(remote_connections[0].clone()),
3319 paths: PathList::default(),
3320 window_id: Some(WindowId::from(9u64)),
3321 }
3322 );
3323 }
3324
3325 #[gpui::test]
3326 async fn test_get_or_create_ssh_project() {
3327 let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project").await;
3328
3329 let host = "example.com".to_string();
3330 let port = Some(22_u16);
3331 let user = Some("user".to_string());
3332
3333 let connection_id = db
3334 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3335 host: host.clone().into(),
3336 port,
3337 username: user.clone(),
3338 ..Default::default()
3339 }))
3340 .await
3341 .unwrap();
3342
3343 // Test that calling the function again with the same parameters returns the same project
3344 let same_connection = db
3345 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3346 host: host.clone().into(),
3347 port,
3348 username: user.clone(),
3349 ..Default::default()
3350 }))
3351 .await
3352 .unwrap();
3353
3354 assert_eq!(connection_id, same_connection);
3355
3356 // Test with different parameters
3357 let host2 = "otherexample.com".to_string();
3358 let port2 = None;
3359 let user2 = Some("otheruser".to_string());
3360
3361 let different_connection = db
3362 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3363 host: host2.clone().into(),
3364 port: port2,
3365 username: user2.clone(),
3366 ..Default::default()
3367 }))
3368 .await
3369 .unwrap();
3370
3371 assert_ne!(connection_id, different_connection);
3372 }
3373
3374 #[gpui::test]
3375 async fn test_get_or_create_ssh_project_with_null_user() {
3376 let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project_with_null_user").await;
3377
3378 let (host, port, user) = ("example.com".to_string(), None, None);
3379
3380 let connection_id = db
3381 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3382 host: host.clone().into(),
3383 port,
3384 username: None,
3385 ..Default::default()
3386 }))
3387 .await
3388 .unwrap();
3389
3390 let same_connection_id = db
3391 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3392 host: host.clone().into(),
3393 port,
3394 username: user.clone(),
3395 ..Default::default()
3396 }))
3397 .await
3398 .unwrap();
3399
3400 assert_eq!(connection_id, same_connection_id);
3401 }
3402
3403 #[gpui::test]
3404 async fn test_get_remote_connections() {
3405 let db = WorkspaceDb::open_test_db("test_get_remote_connections").await;
3406
3407 let connections = [
3408 ("example.com".to_string(), None, None),
3409 (
3410 "anotherexample.com".to_string(),
3411 Some(123_u16),
3412 Some("user2".to_string()),
3413 ),
3414 ("yetanother.com".to_string(), Some(345_u16), None),
3415 ];
3416
3417 let mut ids = Vec::new();
3418 for (host, port, user) in connections.iter() {
3419 ids.push(
3420 db.get_or_create_remote_connection(RemoteConnectionOptions::Ssh(
3421 SshConnectionOptions {
3422 host: host.clone().into(),
3423 port: *port,
3424 username: user.clone(),
3425 ..Default::default()
3426 },
3427 ))
3428 .await
3429 .unwrap(),
3430 );
3431 }
3432
3433 let stored_connections = db.remote_connections().unwrap();
3434 assert_eq!(
3435 stored_connections,
3436 [
3437 (
3438 ids[0],
3439 RemoteConnectionOptions::Ssh(SshConnectionOptions {
3440 host: "example.com".into(),
3441 port: None,
3442 username: None,
3443 ..Default::default()
3444 }),
3445 ),
3446 (
3447 ids[1],
3448 RemoteConnectionOptions::Ssh(SshConnectionOptions {
3449 host: "anotherexample.com".into(),
3450 port: Some(123),
3451 username: Some("user2".into()),
3452 ..Default::default()
3453 }),
3454 ),
3455 (
3456 ids[2],
3457 RemoteConnectionOptions::Ssh(SshConnectionOptions {
3458 host: "yetanother.com".into(),
3459 port: Some(345),
3460 username: None,
3461 ..Default::default()
3462 }),
3463 ),
3464 ]
3465 .into_iter()
3466 .collect::<HashMap<_, _>>(),
3467 );
3468 }
3469
3470 #[gpui::test]
3471 async fn test_simple_split() {
3472 zlog::init_test();
3473
3474 let db = WorkspaceDb::open_test_db("simple_split").await;
3475
3476 // -----------------
3477 // | 1,2 | 5,6 |
3478 // | - - - | |
3479 // | 3,4 | |
3480 // -----------------
3481 let center_pane = group(
3482 Axis::Horizontal,
3483 vec![
3484 group(
3485 Axis::Vertical,
3486 vec![
3487 SerializedPaneGroup::Pane(SerializedPane::new(
3488 vec![
3489 SerializedItem::new("Terminal", 1, false, false),
3490 SerializedItem::new("Terminal", 2, true, false),
3491 ],
3492 false,
3493 0,
3494 )),
3495 SerializedPaneGroup::Pane(SerializedPane::new(
3496 vec![
3497 SerializedItem::new("Terminal", 4, false, false),
3498 SerializedItem::new("Terminal", 3, true, false),
3499 ],
3500 true,
3501 0,
3502 )),
3503 ],
3504 ),
3505 SerializedPaneGroup::Pane(SerializedPane::new(
3506 vec![
3507 SerializedItem::new("Terminal", 5, true, false),
3508 SerializedItem::new("Terminal", 6, false, false),
3509 ],
3510 false,
3511 0,
3512 )),
3513 ],
3514 );
3515
3516 let workspace = default_workspace(&["/tmp"], ¢er_pane);
3517
3518 db.save_workspace(workspace.clone()).await;
3519
3520 let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
3521
3522 assert_eq!(workspace.center_group, new_workspace.center_group);
3523 }
3524
3525 #[gpui::test]
3526 async fn test_cleanup_panes() {
3527 zlog::init_test();
3528
3529 let db = WorkspaceDb::open_test_db("test_cleanup_panes").await;
3530
3531 let center_pane = group(
3532 Axis::Horizontal,
3533 vec![
3534 group(
3535 Axis::Vertical,
3536 vec![
3537 SerializedPaneGroup::Pane(SerializedPane::new(
3538 vec![
3539 SerializedItem::new("Terminal", 1, false, false),
3540 SerializedItem::new("Terminal", 2, true, false),
3541 ],
3542 false,
3543 0,
3544 )),
3545 SerializedPaneGroup::Pane(SerializedPane::new(
3546 vec![
3547 SerializedItem::new("Terminal", 4, false, false),
3548 SerializedItem::new("Terminal", 3, true, false),
3549 ],
3550 true,
3551 0,
3552 )),
3553 ],
3554 ),
3555 SerializedPaneGroup::Pane(SerializedPane::new(
3556 vec![
3557 SerializedItem::new("Terminal", 5, false, false),
3558 SerializedItem::new("Terminal", 6, true, false),
3559 ],
3560 false,
3561 0,
3562 )),
3563 ],
3564 );
3565
3566 let id = &["/tmp"];
3567
3568 let mut workspace = default_workspace(id, ¢er_pane);
3569
3570 db.save_workspace(workspace.clone()).await;
3571
3572 workspace.center_group = group(
3573 Axis::Vertical,
3574 vec![
3575 SerializedPaneGroup::Pane(SerializedPane::new(
3576 vec![
3577 SerializedItem::new("Terminal", 1, false, false),
3578 SerializedItem::new("Terminal", 2, true, false),
3579 ],
3580 false,
3581 0,
3582 )),
3583 SerializedPaneGroup::Pane(SerializedPane::new(
3584 vec![
3585 SerializedItem::new("Terminal", 4, true, false),
3586 SerializedItem::new("Terminal", 3, false, false),
3587 ],
3588 true,
3589 0,
3590 )),
3591 ],
3592 );
3593
3594 db.save_workspace(workspace.clone()).await;
3595
3596 let new_workspace = db.workspace_for_roots(id).unwrap();
3597
3598 assert_eq!(workspace.center_group, new_workspace.center_group);
3599 }
3600
3601 #[gpui::test]
3602 async fn test_empty_workspace_window_bounds() {
3603 zlog::init_test();
3604
3605 let db = WorkspaceDb::open_test_db("test_empty_workspace_window_bounds").await;
3606 let id = db.next_id().await.unwrap();
3607
3608 // Create a workspace with empty paths (empty workspace)
3609 let empty_paths: &[&str] = &[];
3610 let display_uuid = Uuid::new_v4();
3611 let window_bounds = SerializedWindowBounds(WindowBounds::Windowed(Bounds {
3612 origin: point(px(100.0), px(200.0)),
3613 size: size(px(800.0), px(600.0)),
3614 }));
3615
3616 let workspace = SerializedWorkspace {
3617 id,
3618 paths: PathList::new(empty_paths),
3619 location: SerializedWorkspaceLocation::Local,
3620 center_group: Default::default(),
3621 window_bounds: None,
3622 display: None,
3623 docks: Default::default(),
3624 breakpoints: Default::default(),
3625 centered_layout: false,
3626 session_id: None,
3627 window_id: None,
3628 user_toolchains: Default::default(),
3629 };
3630
3631 // Save the workspace (this creates the record with empty paths)
3632 db.save_workspace(workspace.clone()).await;
3633
3634 // Save window bounds separately (as the actual code does via set_window_open_status)
3635 db.set_window_open_status(id, window_bounds, display_uuid)
3636 .await
3637 .unwrap();
3638
3639 // Empty workspaces cannot be retrieved by paths (they'd all match).
3640 // They must be retrieved by workspace_id.
3641 assert!(db.workspace_for_roots(empty_paths).is_none());
3642
3643 // Retrieve using workspace_for_id instead
3644 let retrieved = db.workspace_for_id(id).unwrap();
3645
3646 // Verify window bounds were persisted
3647 assert_eq!(retrieved.id, id);
3648 assert!(retrieved.window_bounds.is_some());
3649 assert_eq!(retrieved.window_bounds.unwrap().0, window_bounds.0);
3650 assert!(retrieved.display.is_some());
3651 assert_eq!(retrieved.display.unwrap(), display_uuid);
3652 }
3653
3654 #[gpui::test]
3655 async fn test_last_session_workspace_locations_groups_by_window_id(
3656 cx: &mut gpui::TestAppContext,
3657 ) {
3658 let dir1 = tempfile::TempDir::with_prefix("dir1").unwrap();
3659 let dir2 = tempfile::TempDir::with_prefix("dir2").unwrap();
3660 let dir3 = tempfile::TempDir::with_prefix("dir3").unwrap();
3661 let dir4 = tempfile::TempDir::with_prefix("dir4").unwrap();
3662 let dir5 = tempfile::TempDir::with_prefix("dir5").unwrap();
3663
3664 let fs = fs::FakeFs::new(cx.executor());
3665 fs.insert_tree(dir1.path(), json!({})).await;
3666 fs.insert_tree(dir2.path(), json!({})).await;
3667 fs.insert_tree(dir3.path(), json!({})).await;
3668 fs.insert_tree(dir4.path(), json!({})).await;
3669 fs.insert_tree(dir5.path(), json!({})).await;
3670
3671 let db =
3672 WorkspaceDb::open_test_db("test_last_session_workspace_locations_groups_by_window_id")
3673 .await;
3674
3675 // Simulate two MultiWorkspace windows each containing two workspaces,
3676 // plus one single-workspace window:
3677 // Window 10: workspace 1, workspace 2
3678 // Window 20: workspace 3, workspace 4
3679 // Window 30: workspace 5 (only one)
3680 //
3681 // On session restore, the caller should be able to group these by
3682 // window_id to reconstruct the MultiWorkspace windows.
3683 let workspaces_data: Vec<(i64, &Path, u64)> = vec![
3684 (1, dir1.path(), 10),
3685 (2, dir2.path(), 10),
3686 (3, dir3.path(), 20),
3687 (4, dir4.path(), 20),
3688 (5, dir5.path(), 30),
3689 ];
3690
3691 for (id, dir, window_id) in &workspaces_data {
3692 db.save_workspace(SerializedWorkspace {
3693 id: WorkspaceId(*id),
3694 paths: PathList::new(&[*dir]),
3695 location: SerializedWorkspaceLocation::Local,
3696 center_group: Default::default(),
3697 window_bounds: Default::default(),
3698 display: Default::default(),
3699 docks: Default::default(),
3700 centered_layout: false,
3701 session_id: Some("test-session".to_owned()),
3702 breakpoints: Default::default(),
3703 window_id: Some(*window_id),
3704 user_toolchains: Default::default(),
3705 })
3706 .await;
3707 }
3708
3709 let locations = db
3710 .last_session_workspace_locations("test-session", None, fs.as_ref())
3711 .await
3712 .unwrap();
3713
3714 // All 5 workspaces should be returned with their window_ids.
3715 assert_eq!(locations.len(), 5);
3716
3717 // Every entry should have a window_id so the caller can group them.
3718 for session_workspace in &locations {
3719 assert!(
3720 session_workspace.window_id.is_some(),
3721 "workspace {:?} missing window_id",
3722 session_workspace.workspace_id
3723 );
3724 }
3725
3726 // Group by window_id, simulating what the restoration code should do.
3727 let mut by_window: HashMap<WindowId, Vec<WorkspaceId>> = HashMap::default();
3728 for session_workspace in &locations {
3729 if let Some(window_id) = session_workspace.window_id {
3730 by_window
3731 .entry(window_id)
3732 .or_default()
3733 .push(session_workspace.workspace_id);
3734 }
3735 }
3736
3737 // Should produce 3 windows, not 5.
3738 assert_eq!(
3739 by_window.len(),
3740 3,
3741 "Expected 3 window groups, got {}: {:?}",
3742 by_window.len(),
3743 by_window
3744 );
3745
3746 // Window 10 should contain workspaces 1 and 2.
3747 let window_10 = by_window.get(&WindowId::from(10u64)).unwrap();
3748 assert_eq!(window_10.len(), 2);
3749 assert!(window_10.contains(&WorkspaceId(1)));
3750 assert!(window_10.contains(&WorkspaceId(2)));
3751
3752 // Window 20 should contain workspaces 3 and 4.
3753 let window_20 = by_window.get(&WindowId::from(20u64)).unwrap();
3754 assert_eq!(window_20.len(), 2);
3755 assert!(window_20.contains(&WorkspaceId(3)));
3756 assert!(window_20.contains(&WorkspaceId(4)));
3757
3758 // Window 30 should contain only workspace 5.
3759 let window_30 = by_window.get(&WindowId::from(30u64)).unwrap();
3760 assert_eq!(window_30.len(), 1);
3761 assert!(window_30.contains(&WorkspaceId(5)));
3762 }
3763
3764 #[gpui::test]
3765 async fn test_read_serialized_multi_workspaces_with_state() {
3766 use crate::persistence::model::MultiWorkspaceState;
3767
3768 // Write multi-workspace state for two windows via the scoped KVP.
3769 let window_10 = WindowId::from(10u64);
3770 let window_20 = WindowId::from(20u64);
3771
3772 write_multi_workspace_state(
3773 window_10,
3774 MultiWorkspaceState {
3775 active_workspace_id: Some(WorkspaceId(2)),
3776 sidebar_open: true,
3777 },
3778 )
3779 .await;
3780
3781 write_multi_workspace_state(
3782 window_20,
3783 MultiWorkspaceState {
3784 active_workspace_id: Some(WorkspaceId(3)),
3785 sidebar_open: false,
3786 },
3787 )
3788 .await;
3789
3790 // Build session workspaces: two in window 10, one in window 20, one with no window.
3791 let session_workspaces = vec![
3792 SessionWorkspace {
3793 workspace_id: WorkspaceId(1),
3794 location: SerializedWorkspaceLocation::Local,
3795 paths: PathList::new(&["/a"]),
3796 window_id: Some(window_10),
3797 },
3798 SessionWorkspace {
3799 workspace_id: WorkspaceId(2),
3800 location: SerializedWorkspaceLocation::Local,
3801 paths: PathList::new(&["/b"]),
3802 window_id: Some(window_10),
3803 },
3804 SessionWorkspace {
3805 workspace_id: WorkspaceId(3),
3806 location: SerializedWorkspaceLocation::Local,
3807 paths: PathList::new(&["/c"]),
3808 window_id: Some(window_20),
3809 },
3810 SessionWorkspace {
3811 workspace_id: WorkspaceId(4),
3812 location: SerializedWorkspaceLocation::Local,
3813 paths: PathList::new(&["/d"]),
3814 window_id: None,
3815 },
3816 ];
3817
3818 let results = read_serialized_multi_workspaces(session_workspaces);
3819
3820 // Should produce 3 groups: window 10, window 20, and the orphan.
3821 assert_eq!(results.len(), 3);
3822
3823 // Window 10 group: 2 workspaces, active_workspace_id = 2, sidebar open.
3824 let group_10 = &results[0];
3825 assert_eq!(group_10.workspaces.len(), 2);
3826 assert_eq!(group_10.state.active_workspace_id, Some(WorkspaceId(2)));
3827 assert_eq!(group_10.state.sidebar_open, true);
3828
3829 // Window 20 group: 1 workspace, active_workspace_id = 3, sidebar closed.
3830 let group_20 = &results[1];
3831 assert_eq!(group_20.workspaces.len(), 1);
3832 assert_eq!(group_20.state.active_workspace_id, Some(WorkspaceId(3)));
3833 assert_eq!(group_20.state.sidebar_open, false);
3834
3835 // Orphan group: no window_id, so state is default.
3836 let group_none = &results[2];
3837 assert_eq!(group_none.workspaces.len(), 1);
3838 assert_eq!(group_none.state.active_workspace_id, None);
3839 assert_eq!(group_none.state.sidebar_open, false);
3840 }
3841}