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