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