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