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