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