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