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 ) -> anyhow::Result<()> {
1923 use anyhow::Context as _;
1924 use db::sqlez::statement::Statement;
1925 use itertools::Itertools as _;
1926
1927 DB.clear_trusted_worktrees()
1928 .await
1929 .context("clearing previous trust state")?;
1930
1931 let trusted_worktrees = trusted_worktrees
1932 .into_iter()
1933 .flat_map(|(host, abs_paths)| {
1934 abs_paths
1935 .into_iter()
1936 .map(move |abs_path| (Some(abs_path), host.clone()))
1937 })
1938 .collect::<Vec<_>>();
1939 let mut first_worktree;
1940 let mut last_worktree = 0_usize;
1941 for (count, placeholders) in std::iter::once("(?, ?, ?)")
1942 .cycle()
1943 .take(trusted_worktrees.len())
1944 .chunks(MAX_QUERY_PLACEHOLDERS / 3)
1945 .into_iter()
1946 .map(|chunk| {
1947 let mut count = 0;
1948 let placeholders = chunk
1949 .inspect(|_| {
1950 count += 1;
1951 })
1952 .join(", ");
1953 (count, placeholders)
1954 })
1955 .collect::<Vec<_>>()
1956 {
1957 first_worktree = last_worktree;
1958 last_worktree = last_worktree + count;
1959 let query = format!(
1960 r#"INSERT INTO trusted_worktrees(absolute_path, user_name, host_name)
1961VALUES {placeholders};"#
1962 );
1963
1964 let trusted_worktrees = trusted_worktrees[first_worktree..last_worktree].to_vec();
1965 self.write(move |conn| {
1966 let mut statement = Statement::prepare(conn, query)?;
1967 let mut next_index = 1;
1968 for (abs_path, host) in trusted_worktrees {
1969 let abs_path = abs_path.as_ref().map(|abs_path| abs_path.to_string_lossy());
1970 next_index = statement.bind(
1971 &abs_path.as_ref().map(|abs_path| abs_path.as_ref()),
1972 next_index,
1973 )?;
1974 next_index = statement.bind(
1975 &host
1976 .as_ref()
1977 .and_then(|host| Some(host.user_name.as_ref()?.as_str())),
1978 next_index,
1979 )?;
1980 next_index = statement.bind(
1981 &host.as_ref().map(|host| host.host_identifier.as_str()),
1982 next_index,
1983 )?;
1984 }
1985 statement.exec()
1986 })
1987 .await
1988 .context("inserting new trusted state")?;
1989 }
1990 Ok(())
1991 }
1992
1993 pub fn fetch_trusted_worktrees(
1994 &self,
1995 worktree_store: Option<Entity<WorktreeStore>>,
1996 host: Option<RemoteHostLocation>,
1997 cx: &App,
1998 ) -> Result<HashMap<Option<RemoteHostLocation>, HashSet<PathTrust>>> {
1999 let trusted_worktrees = DB.trusted_worktrees()?;
2000 Ok(trusted_worktrees
2001 .into_iter()
2002 .filter_map(|(abs_path, user_name, host_name)| {
2003 let db_host = match (user_name, host_name) {
2004 (_, None) => None,
2005 (None, Some(host_name)) => Some(RemoteHostLocation {
2006 user_name: None,
2007 host_identifier: SharedString::new(host_name),
2008 }),
2009 (Some(user_name), Some(host_name)) => Some(RemoteHostLocation {
2010 user_name: Some(SharedString::new(user_name)),
2011 host_identifier: SharedString::new(host_name),
2012 }),
2013 };
2014
2015 let abs_path = abs_path?;
2016 Some(if db_host != host {
2017 (db_host, PathTrust::AbsPath(abs_path))
2018 } else if let Some(worktree_store) = &worktree_store {
2019 find_worktree_in_store(worktree_store.read(cx), &abs_path, cx)
2020 .map(PathTrust::Worktree)
2021 .map(|trusted_worktree| (host.clone(), trusted_worktree))
2022 .unwrap_or_else(|| (db_host.clone(), PathTrust::AbsPath(abs_path)))
2023 } else {
2024 (db_host, PathTrust::AbsPath(abs_path))
2025 })
2026 })
2027 .fold(HashMap::default(), |mut acc, (remote_host, path_trust)| {
2028 acc.entry(remote_host)
2029 .or_insert_with(HashSet::default)
2030 .insert(path_trust);
2031 acc
2032 }))
2033 }
2034
2035 query! {
2036 fn trusted_worktrees() -> Result<Vec<(Option<PathBuf>, Option<String>, Option<String>)>> {
2037 SELECT absolute_path, user_name, host_name
2038 FROM trusted_worktrees
2039 }
2040 }
2041
2042 query! {
2043 pub async fn clear_trusted_worktrees() -> Result<()> {
2044 DELETE FROM trusted_worktrees
2045 }
2046 }
2047}
2048
2049pub fn delete_unloaded_items(
2050 alive_items: Vec<ItemId>,
2051 workspace_id: WorkspaceId,
2052 table: &'static str,
2053 db: &ThreadSafeConnection,
2054 cx: &mut App,
2055) -> Task<Result<()>> {
2056 let db = db.clone();
2057 cx.spawn(async move |_| {
2058 let placeholders = alive_items
2059 .iter()
2060 .map(|_| "?")
2061 .collect::<Vec<&str>>()
2062 .join(", ");
2063
2064 let query = format!(
2065 "DELETE FROM {table} WHERE workspace_id = ? AND item_id NOT IN ({placeholders})"
2066 );
2067
2068 db.write(move |conn| {
2069 let mut statement = Statement::prepare(conn, query)?;
2070 let mut next_index = statement.bind(&workspace_id, 1)?;
2071 for id in alive_items {
2072 next_index = statement.bind(&id, next_index)?;
2073 }
2074 statement.exec()
2075 })
2076 .await
2077 })
2078}
2079
2080#[cfg(test)]
2081mod tests {
2082 use super::*;
2083 use crate::persistence::model::{
2084 SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace,
2085 };
2086 use gpui;
2087 use pretty_assertions::assert_eq;
2088 use remote::SshConnectionOptions;
2089 use std::{thread, time::Duration};
2090
2091 #[gpui::test]
2092 async fn test_breakpoints() {
2093 zlog::init_test();
2094
2095 let db = WorkspaceDb::open_test_db("test_breakpoints").await;
2096 let id = db.next_id().await.unwrap();
2097
2098 let path = Path::new("/tmp/test.rs");
2099
2100 let breakpoint = Breakpoint {
2101 position: 123,
2102 message: None,
2103 state: BreakpointState::Enabled,
2104 condition: None,
2105 hit_condition: None,
2106 };
2107
2108 let log_breakpoint = Breakpoint {
2109 position: 456,
2110 message: Some("Test log message".into()),
2111 state: BreakpointState::Enabled,
2112 condition: None,
2113 hit_condition: None,
2114 };
2115
2116 let disable_breakpoint = Breakpoint {
2117 position: 578,
2118 message: None,
2119 state: BreakpointState::Disabled,
2120 condition: None,
2121 hit_condition: None,
2122 };
2123
2124 let condition_breakpoint = Breakpoint {
2125 position: 789,
2126 message: None,
2127 state: BreakpointState::Enabled,
2128 condition: Some("x > 5".into()),
2129 hit_condition: None,
2130 };
2131
2132 let hit_condition_breakpoint = Breakpoint {
2133 position: 999,
2134 message: None,
2135 state: BreakpointState::Enabled,
2136 condition: None,
2137 hit_condition: Some(">= 3".into()),
2138 };
2139
2140 let workspace = SerializedWorkspace {
2141 id,
2142 paths: PathList::new(&["/tmp"]),
2143 location: SerializedWorkspaceLocation::Local,
2144 center_group: Default::default(),
2145 window_bounds: Default::default(),
2146 display: Default::default(),
2147 docks: Default::default(),
2148 centered_layout: false,
2149 breakpoints: {
2150 let mut map = collections::BTreeMap::default();
2151 map.insert(
2152 Arc::from(path),
2153 vec![
2154 SourceBreakpoint {
2155 row: breakpoint.position,
2156 path: Arc::from(path),
2157 message: breakpoint.message.clone(),
2158 state: breakpoint.state,
2159 condition: breakpoint.condition.clone(),
2160 hit_condition: breakpoint.hit_condition.clone(),
2161 },
2162 SourceBreakpoint {
2163 row: log_breakpoint.position,
2164 path: Arc::from(path),
2165 message: log_breakpoint.message.clone(),
2166 state: log_breakpoint.state,
2167 condition: log_breakpoint.condition.clone(),
2168 hit_condition: log_breakpoint.hit_condition.clone(),
2169 },
2170 SourceBreakpoint {
2171 row: disable_breakpoint.position,
2172 path: Arc::from(path),
2173 message: disable_breakpoint.message.clone(),
2174 state: disable_breakpoint.state,
2175 condition: disable_breakpoint.condition.clone(),
2176 hit_condition: disable_breakpoint.hit_condition.clone(),
2177 },
2178 SourceBreakpoint {
2179 row: condition_breakpoint.position,
2180 path: Arc::from(path),
2181 message: condition_breakpoint.message.clone(),
2182 state: condition_breakpoint.state,
2183 condition: condition_breakpoint.condition.clone(),
2184 hit_condition: condition_breakpoint.hit_condition.clone(),
2185 },
2186 SourceBreakpoint {
2187 row: hit_condition_breakpoint.position,
2188 path: Arc::from(path),
2189 message: hit_condition_breakpoint.message.clone(),
2190 state: hit_condition_breakpoint.state,
2191 condition: hit_condition_breakpoint.condition.clone(),
2192 hit_condition: hit_condition_breakpoint.hit_condition.clone(),
2193 },
2194 ],
2195 );
2196 map
2197 },
2198 session_id: None,
2199 window_id: None,
2200 user_toolchains: Default::default(),
2201 };
2202
2203 db.save_workspace(workspace.clone()).await;
2204
2205 let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
2206 let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(path)).unwrap();
2207
2208 assert_eq!(loaded_breakpoints.len(), 5);
2209
2210 // normal breakpoint
2211 assert_eq!(loaded_breakpoints[0].row, breakpoint.position);
2212 assert_eq!(loaded_breakpoints[0].message, breakpoint.message);
2213 assert_eq!(loaded_breakpoints[0].condition, breakpoint.condition);
2214 assert_eq!(
2215 loaded_breakpoints[0].hit_condition,
2216 breakpoint.hit_condition
2217 );
2218 assert_eq!(loaded_breakpoints[0].state, breakpoint.state);
2219 assert_eq!(loaded_breakpoints[0].path, Arc::from(path));
2220
2221 // enabled breakpoint
2222 assert_eq!(loaded_breakpoints[1].row, log_breakpoint.position);
2223 assert_eq!(loaded_breakpoints[1].message, log_breakpoint.message);
2224 assert_eq!(loaded_breakpoints[1].condition, log_breakpoint.condition);
2225 assert_eq!(
2226 loaded_breakpoints[1].hit_condition,
2227 log_breakpoint.hit_condition
2228 );
2229 assert_eq!(loaded_breakpoints[1].state, log_breakpoint.state);
2230 assert_eq!(loaded_breakpoints[1].path, Arc::from(path));
2231
2232 // disable breakpoint
2233 assert_eq!(loaded_breakpoints[2].row, disable_breakpoint.position);
2234 assert_eq!(loaded_breakpoints[2].message, disable_breakpoint.message);
2235 assert_eq!(
2236 loaded_breakpoints[2].condition,
2237 disable_breakpoint.condition
2238 );
2239 assert_eq!(
2240 loaded_breakpoints[2].hit_condition,
2241 disable_breakpoint.hit_condition
2242 );
2243 assert_eq!(loaded_breakpoints[2].state, disable_breakpoint.state);
2244 assert_eq!(loaded_breakpoints[2].path, Arc::from(path));
2245
2246 // condition breakpoint
2247 assert_eq!(loaded_breakpoints[3].row, condition_breakpoint.position);
2248 assert_eq!(loaded_breakpoints[3].message, condition_breakpoint.message);
2249 assert_eq!(
2250 loaded_breakpoints[3].condition,
2251 condition_breakpoint.condition
2252 );
2253 assert_eq!(
2254 loaded_breakpoints[3].hit_condition,
2255 condition_breakpoint.hit_condition
2256 );
2257 assert_eq!(loaded_breakpoints[3].state, condition_breakpoint.state);
2258 assert_eq!(loaded_breakpoints[3].path, Arc::from(path));
2259
2260 // hit condition breakpoint
2261 assert_eq!(loaded_breakpoints[4].row, hit_condition_breakpoint.position);
2262 assert_eq!(
2263 loaded_breakpoints[4].message,
2264 hit_condition_breakpoint.message
2265 );
2266 assert_eq!(
2267 loaded_breakpoints[4].condition,
2268 hit_condition_breakpoint.condition
2269 );
2270 assert_eq!(
2271 loaded_breakpoints[4].hit_condition,
2272 hit_condition_breakpoint.hit_condition
2273 );
2274 assert_eq!(loaded_breakpoints[4].state, hit_condition_breakpoint.state);
2275 assert_eq!(loaded_breakpoints[4].path, Arc::from(path));
2276 }
2277
2278 #[gpui::test]
2279 async fn test_remove_last_breakpoint() {
2280 zlog::init_test();
2281
2282 let db = WorkspaceDb::open_test_db("test_remove_last_breakpoint").await;
2283 let id = db.next_id().await.unwrap();
2284
2285 let singular_path = Path::new("/tmp/test_remove_last_breakpoint.rs");
2286
2287 let breakpoint_to_remove = Breakpoint {
2288 position: 100,
2289 message: None,
2290 state: BreakpointState::Enabled,
2291 condition: None,
2292 hit_condition: None,
2293 };
2294
2295 let workspace = SerializedWorkspace {
2296 id,
2297 paths: PathList::new(&["/tmp"]),
2298 location: SerializedWorkspaceLocation::Local,
2299 center_group: Default::default(),
2300 window_bounds: Default::default(),
2301 display: Default::default(),
2302 docks: Default::default(),
2303 centered_layout: false,
2304 breakpoints: {
2305 let mut map = collections::BTreeMap::default();
2306 map.insert(
2307 Arc::from(singular_path),
2308 vec![SourceBreakpoint {
2309 row: breakpoint_to_remove.position,
2310 path: Arc::from(singular_path),
2311 message: None,
2312 state: BreakpointState::Enabled,
2313 condition: None,
2314 hit_condition: None,
2315 }],
2316 );
2317 map
2318 },
2319 session_id: None,
2320 window_id: None,
2321 user_toolchains: Default::default(),
2322 };
2323
2324 db.save_workspace(workspace.clone()).await;
2325
2326 let loaded = db.workspace_for_roots(&["/tmp"]).unwrap();
2327 let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(singular_path)).unwrap();
2328
2329 assert_eq!(loaded_breakpoints.len(), 1);
2330 assert_eq!(loaded_breakpoints[0].row, breakpoint_to_remove.position);
2331 assert_eq!(loaded_breakpoints[0].message, breakpoint_to_remove.message);
2332 assert_eq!(
2333 loaded_breakpoints[0].condition,
2334 breakpoint_to_remove.condition
2335 );
2336 assert_eq!(
2337 loaded_breakpoints[0].hit_condition,
2338 breakpoint_to_remove.hit_condition
2339 );
2340 assert_eq!(loaded_breakpoints[0].state, breakpoint_to_remove.state);
2341 assert_eq!(loaded_breakpoints[0].path, Arc::from(singular_path));
2342
2343 let workspace_without_breakpoint = SerializedWorkspace {
2344 id,
2345 paths: PathList::new(&["/tmp"]),
2346 location: SerializedWorkspaceLocation::Local,
2347 center_group: Default::default(),
2348 window_bounds: Default::default(),
2349 display: Default::default(),
2350 docks: Default::default(),
2351 centered_layout: false,
2352 breakpoints: collections::BTreeMap::default(),
2353 session_id: None,
2354 window_id: None,
2355 user_toolchains: Default::default(),
2356 };
2357
2358 db.save_workspace(workspace_without_breakpoint.clone())
2359 .await;
2360
2361 let loaded_after_remove = db.workspace_for_roots(&["/tmp"]).unwrap();
2362 let empty_breakpoints = loaded_after_remove
2363 .breakpoints
2364 .get(&Arc::from(singular_path));
2365
2366 assert!(empty_breakpoints.is_none());
2367 }
2368
2369 #[gpui::test]
2370 async fn test_next_id_stability() {
2371 zlog::init_test();
2372
2373 let db = WorkspaceDb::open_test_db("test_next_id_stability").await;
2374
2375 db.write(|conn| {
2376 conn.migrate(
2377 "test_table",
2378 &[sql!(
2379 CREATE TABLE test_table(
2380 text TEXT,
2381 workspace_id INTEGER,
2382 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
2383 ON DELETE CASCADE
2384 ) STRICT;
2385 )],
2386 |_, _, _| false,
2387 )
2388 .unwrap();
2389 })
2390 .await;
2391
2392 let id = db.next_id().await.unwrap();
2393 // Assert the empty row got inserted
2394 assert_eq!(
2395 Some(id),
2396 db.select_row_bound::<WorkspaceId, WorkspaceId>(sql!(
2397 SELECT workspace_id FROM workspaces WHERE workspace_id = ?
2398 ))
2399 .unwrap()(id)
2400 .unwrap()
2401 );
2402
2403 db.write(move |conn| {
2404 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2405 .unwrap()(("test-text-1", id))
2406 .unwrap()
2407 })
2408 .await;
2409
2410 let test_text_1 = db
2411 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2412 .unwrap()(1)
2413 .unwrap()
2414 .unwrap();
2415 assert_eq!(test_text_1, "test-text-1");
2416 }
2417
2418 #[gpui::test]
2419 async fn test_workspace_id_stability() {
2420 zlog::init_test();
2421
2422 let db = WorkspaceDb::open_test_db("test_workspace_id_stability").await;
2423
2424 db.write(|conn| {
2425 conn.migrate(
2426 "test_table",
2427 &[sql!(
2428 CREATE TABLE test_table(
2429 text TEXT,
2430 workspace_id INTEGER,
2431 FOREIGN KEY(workspace_id)
2432 REFERENCES workspaces(workspace_id)
2433 ON DELETE CASCADE
2434 ) STRICT;)],
2435 |_, _, _| false,
2436 )
2437 })
2438 .await
2439 .unwrap();
2440
2441 let mut workspace_1 = SerializedWorkspace {
2442 id: WorkspaceId(1),
2443 paths: PathList::new(&["/tmp", "/tmp2"]),
2444 location: SerializedWorkspaceLocation::Local,
2445 center_group: Default::default(),
2446 window_bounds: Default::default(),
2447 display: Default::default(),
2448 docks: Default::default(),
2449 centered_layout: false,
2450 breakpoints: Default::default(),
2451 session_id: None,
2452 window_id: None,
2453 user_toolchains: Default::default(),
2454 };
2455
2456 let workspace_2 = SerializedWorkspace {
2457 id: WorkspaceId(2),
2458 paths: PathList::new(&["/tmp"]),
2459 location: SerializedWorkspaceLocation::Local,
2460 center_group: Default::default(),
2461 window_bounds: Default::default(),
2462 display: Default::default(),
2463 docks: Default::default(),
2464 centered_layout: false,
2465 breakpoints: Default::default(),
2466 session_id: None,
2467 window_id: None,
2468 user_toolchains: Default::default(),
2469 };
2470
2471 db.save_workspace(workspace_1.clone()).await;
2472
2473 db.write(|conn| {
2474 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2475 .unwrap()(("test-text-1", 1))
2476 .unwrap();
2477 })
2478 .await;
2479
2480 db.save_workspace(workspace_2.clone()).await;
2481
2482 db.write(|conn| {
2483 conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?)))
2484 .unwrap()(("test-text-2", 2))
2485 .unwrap();
2486 })
2487 .await;
2488
2489 workspace_1.paths = PathList::new(&["/tmp", "/tmp3"]);
2490 db.save_workspace(workspace_1.clone()).await;
2491 db.save_workspace(workspace_1).await;
2492 db.save_workspace(workspace_2).await;
2493
2494 let test_text_2 = db
2495 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2496 .unwrap()(2)
2497 .unwrap()
2498 .unwrap();
2499 assert_eq!(test_text_2, "test-text-2");
2500
2501 let test_text_1 = db
2502 .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?))
2503 .unwrap()(1)
2504 .unwrap()
2505 .unwrap();
2506 assert_eq!(test_text_1, "test-text-1");
2507 }
2508
2509 fn group(axis: Axis, children: Vec<SerializedPaneGroup>) -> SerializedPaneGroup {
2510 SerializedPaneGroup::Group {
2511 axis: SerializedAxis(axis),
2512 flexes: None,
2513 children,
2514 }
2515 }
2516
2517 #[gpui::test]
2518 async fn test_full_workspace_serialization() {
2519 zlog::init_test();
2520
2521 let db = WorkspaceDb::open_test_db("test_full_workspace_serialization").await;
2522
2523 // -----------------
2524 // | 1,2 | 5,6 |
2525 // | - - - | |
2526 // | 3,4 | |
2527 // -----------------
2528 let center_group = group(
2529 Axis::Horizontal,
2530 vec![
2531 group(
2532 Axis::Vertical,
2533 vec![
2534 SerializedPaneGroup::Pane(SerializedPane::new(
2535 vec![
2536 SerializedItem::new("Terminal", 5, false, false),
2537 SerializedItem::new("Terminal", 6, true, false),
2538 ],
2539 false,
2540 0,
2541 )),
2542 SerializedPaneGroup::Pane(SerializedPane::new(
2543 vec![
2544 SerializedItem::new("Terminal", 7, true, false),
2545 SerializedItem::new("Terminal", 8, false, false),
2546 ],
2547 false,
2548 0,
2549 )),
2550 ],
2551 ),
2552 SerializedPaneGroup::Pane(SerializedPane::new(
2553 vec![
2554 SerializedItem::new("Terminal", 9, false, false),
2555 SerializedItem::new("Terminal", 10, true, false),
2556 ],
2557 false,
2558 0,
2559 )),
2560 ],
2561 );
2562
2563 let workspace = SerializedWorkspace {
2564 id: WorkspaceId(5),
2565 paths: PathList::new(&["/tmp", "/tmp2"]),
2566 location: SerializedWorkspaceLocation::Local,
2567 center_group,
2568 window_bounds: Default::default(),
2569 breakpoints: Default::default(),
2570 display: Default::default(),
2571 docks: Default::default(),
2572 centered_layout: false,
2573 session_id: None,
2574 window_id: Some(999),
2575 user_toolchains: Default::default(),
2576 };
2577
2578 db.save_workspace(workspace.clone()).await;
2579
2580 let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]);
2581 assert_eq!(workspace, round_trip_workspace.unwrap());
2582
2583 // Test guaranteed duplicate IDs
2584 db.save_workspace(workspace.clone()).await;
2585 db.save_workspace(workspace.clone()).await;
2586
2587 let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]);
2588 assert_eq!(workspace, round_trip_workspace.unwrap());
2589 }
2590
2591 #[gpui::test]
2592 async fn test_workspace_assignment() {
2593 zlog::init_test();
2594
2595 let db = WorkspaceDb::open_test_db("test_basic_functionality").await;
2596
2597 let workspace_1 = SerializedWorkspace {
2598 id: WorkspaceId(1),
2599 paths: PathList::new(&["/tmp", "/tmp2"]),
2600 location: SerializedWorkspaceLocation::Local,
2601 center_group: Default::default(),
2602 window_bounds: Default::default(),
2603 breakpoints: Default::default(),
2604 display: Default::default(),
2605 docks: Default::default(),
2606 centered_layout: false,
2607 session_id: None,
2608 window_id: Some(1),
2609 user_toolchains: Default::default(),
2610 };
2611
2612 let mut workspace_2 = SerializedWorkspace {
2613 id: WorkspaceId(2),
2614 paths: PathList::new(&["/tmp"]),
2615 location: SerializedWorkspaceLocation::Local,
2616 center_group: Default::default(),
2617 window_bounds: Default::default(),
2618 display: Default::default(),
2619 docks: Default::default(),
2620 centered_layout: false,
2621 breakpoints: Default::default(),
2622 session_id: None,
2623 window_id: Some(2),
2624 user_toolchains: Default::default(),
2625 };
2626
2627 db.save_workspace(workspace_1.clone()).await;
2628 db.save_workspace(workspace_2.clone()).await;
2629
2630 // Test that paths are treated as a set
2631 assert_eq!(
2632 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2633 workspace_1
2634 );
2635 assert_eq!(
2636 db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(),
2637 workspace_1
2638 );
2639
2640 // Make sure that other keys work
2641 assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2);
2642 assert_eq!(db.workspace_for_roots(&["/tmp3", "/tmp2", "/tmp4"]), None);
2643
2644 // Test 'mutate' case of updating a pre-existing id
2645 workspace_2.paths = PathList::new(&["/tmp", "/tmp2"]);
2646
2647 db.save_workspace(workspace_2.clone()).await;
2648 assert_eq!(
2649 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2650 workspace_2
2651 );
2652
2653 // Test other mechanism for mutating
2654 let mut workspace_3 = SerializedWorkspace {
2655 id: WorkspaceId(3),
2656 paths: PathList::new(&["/tmp2", "/tmp"]),
2657 location: SerializedWorkspaceLocation::Local,
2658 center_group: Default::default(),
2659 window_bounds: Default::default(),
2660 breakpoints: Default::default(),
2661 display: Default::default(),
2662 docks: Default::default(),
2663 centered_layout: false,
2664 session_id: None,
2665 window_id: Some(3),
2666 user_toolchains: Default::default(),
2667 };
2668
2669 db.save_workspace(workspace_3.clone()).await;
2670 assert_eq!(
2671 db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(),
2672 workspace_3
2673 );
2674
2675 // Make sure that updating paths differently also works
2676 workspace_3.paths = PathList::new(&["/tmp3", "/tmp4", "/tmp2"]);
2677 db.save_workspace(workspace_3.clone()).await;
2678 assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None);
2679 assert_eq!(
2680 db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"])
2681 .unwrap(),
2682 workspace_3
2683 );
2684 }
2685
2686 #[gpui::test]
2687 async fn test_session_workspaces() {
2688 zlog::init_test();
2689
2690 let db = WorkspaceDb::open_test_db("test_serializing_workspaces_session_id").await;
2691
2692 let workspace_1 = SerializedWorkspace {
2693 id: WorkspaceId(1),
2694 paths: PathList::new(&["/tmp1"]),
2695 location: SerializedWorkspaceLocation::Local,
2696 center_group: Default::default(),
2697 window_bounds: Default::default(),
2698 display: Default::default(),
2699 docks: Default::default(),
2700 centered_layout: false,
2701 breakpoints: Default::default(),
2702 session_id: Some("session-id-1".to_owned()),
2703 window_id: Some(10),
2704 user_toolchains: Default::default(),
2705 };
2706
2707 let workspace_2 = SerializedWorkspace {
2708 id: WorkspaceId(2),
2709 paths: PathList::new(&["/tmp2"]),
2710 location: SerializedWorkspaceLocation::Local,
2711 center_group: Default::default(),
2712 window_bounds: Default::default(),
2713 display: Default::default(),
2714 docks: Default::default(),
2715 centered_layout: false,
2716 breakpoints: Default::default(),
2717 session_id: Some("session-id-1".to_owned()),
2718 window_id: Some(20),
2719 user_toolchains: Default::default(),
2720 };
2721
2722 let workspace_3 = SerializedWorkspace {
2723 id: WorkspaceId(3),
2724 paths: PathList::new(&["/tmp3"]),
2725 location: SerializedWorkspaceLocation::Local,
2726 center_group: Default::default(),
2727 window_bounds: Default::default(),
2728 display: Default::default(),
2729 docks: Default::default(),
2730 centered_layout: false,
2731 breakpoints: Default::default(),
2732 session_id: Some("session-id-2".to_owned()),
2733 window_id: Some(30),
2734 user_toolchains: Default::default(),
2735 };
2736
2737 let workspace_4 = SerializedWorkspace {
2738 id: WorkspaceId(4),
2739 paths: PathList::new(&["/tmp4"]),
2740 location: SerializedWorkspaceLocation::Local,
2741 center_group: Default::default(),
2742 window_bounds: Default::default(),
2743 display: Default::default(),
2744 docks: Default::default(),
2745 centered_layout: false,
2746 breakpoints: Default::default(),
2747 session_id: None,
2748 window_id: None,
2749 user_toolchains: Default::default(),
2750 };
2751
2752 let connection_id = db
2753 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
2754 host: "my-host".into(),
2755 port: Some(1234),
2756 ..Default::default()
2757 }))
2758 .await
2759 .unwrap();
2760
2761 let workspace_5 = SerializedWorkspace {
2762 id: WorkspaceId(5),
2763 paths: PathList::default(),
2764 location: SerializedWorkspaceLocation::Remote(
2765 db.remote_connection(connection_id).unwrap(),
2766 ),
2767 center_group: Default::default(),
2768 window_bounds: Default::default(),
2769 display: Default::default(),
2770 docks: Default::default(),
2771 centered_layout: false,
2772 breakpoints: Default::default(),
2773 session_id: Some("session-id-2".to_owned()),
2774 window_id: Some(50),
2775 user_toolchains: Default::default(),
2776 };
2777
2778 let workspace_6 = SerializedWorkspace {
2779 id: WorkspaceId(6),
2780 paths: PathList::new(&["/tmp6c", "/tmp6b", "/tmp6a"]),
2781 location: SerializedWorkspaceLocation::Local,
2782 center_group: Default::default(),
2783 window_bounds: Default::default(),
2784 breakpoints: Default::default(),
2785 display: Default::default(),
2786 docks: Default::default(),
2787 centered_layout: false,
2788 session_id: Some("session-id-3".to_owned()),
2789 window_id: Some(60),
2790 user_toolchains: Default::default(),
2791 };
2792
2793 db.save_workspace(workspace_1.clone()).await;
2794 thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
2795 db.save_workspace(workspace_2.clone()).await;
2796 db.save_workspace(workspace_3.clone()).await;
2797 thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment
2798 db.save_workspace(workspace_4.clone()).await;
2799 db.save_workspace(workspace_5.clone()).await;
2800 db.save_workspace(workspace_6.clone()).await;
2801
2802 let locations = db.session_workspaces("session-id-1".to_owned()).unwrap();
2803 assert_eq!(locations.len(), 2);
2804 assert_eq!(locations[0].0, PathList::new(&["/tmp2"]));
2805 assert_eq!(locations[0].1, Some(20));
2806 assert_eq!(locations[1].0, PathList::new(&["/tmp1"]));
2807 assert_eq!(locations[1].1, Some(10));
2808
2809 let locations = db.session_workspaces("session-id-2".to_owned()).unwrap();
2810 assert_eq!(locations.len(), 2);
2811 assert_eq!(locations[0].0, PathList::default());
2812 assert_eq!(locations[0].1, Some(50));
2813 assert_eq!(locations[0].2, Some(connection_id));
2814 assert_eq!(locations[1].0, PathList::new(&["/tmp3"]));
2815 assert_eq!(locations[1].1, Some(30));
2816
2817 let locations = db.session_workspaces("session-id-3".to_owned()).unwrap();
2818 assert_eq!(locations.len(), 1);
2819 assert_eq!(
2820 locations[0].0,
2821 PathList::new(&["/tmp6c", "/tmp6b", "/tmp6a"]),
2822 );
2823 assert_eq!(locations[0].1, Some(60));
2824 }
2825
2826 fn default_workspace<P: AsRef<Path>>(
2827 paths: &[P],
2828 center_group: &SerializedPaneGroup,
2829 ) -> SerializedWorkspace {
2830 SerializedWorkspace {
2831 id: WorkspaceId(4),
2832 paths: PathList::new(paths),
2833 location: SerializedWorkspaceLocation::Local,
2834 center_group: center_group.clone(),
2835 window_bounds: Default::default(),
2836 display: Default::default(),
2837 docks: Default::default(),
2838 breakpoints: Default::default(),
2839 centered_layout: false,
2840 session_id: None,
2841 window_id: None,
2842 user_toolchains: Default::default(),
2843 }
2844 }
2845
2846 #[gpui::test]
2847 async fn test_last_session_workspace_locations() {
2848 let dir1 = tempfile::TempDir::with_prefix("dir1").unwrap();
2849 let dir2 = tempfile::TempDir::with_prefix("dir2").unwrap();
2850 let dir3 = tempfile::TempDir::with_prefix("dir3").unwrap();
2851 let dir4 = tempfile::TempDir::with_prefix("dir4").unwrap();
2852
2853 let db =
2854 WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces").await;
2855
2856 let workspaces = [
2857 (1, vec![dir1.path()], 9),
2858 (2, vec![dir2.path()], 5),
2859 (3, vec![dir3.path()], 8),
2860 (4, vec![dir4.path()], 2),
2861 (5, vec![dir1.path(), dir2.path(), dir3.path()], 3),
2862 (6, vec![dir4.path(), dir3.path(), dir2.path()], 4),
2863 ]
2864 .into_iter()
2865 .map(|(id, paths, window_id)| SerializedWorkspace {
2866 id: WorkspaceId(id),
2867 paths: PathList::new(paths.as_slice()),
2868 location: SerializedWorkspaceLocation::Local,
2869 center_group: Default::default(),
2870 window_bounds: Default::default(),
2871 display: Default::default(),
2872 docks: Default::default(),
2873 centered_layout: false,
2874 session_id: Some("one-session".to_owned()),
2875 breakpoints: Default::default(),
2876 window_id: Some(window_id),
2877 user_toolchains: Default::default(),
2878 })
2879 .collect::<Vec<_>>();
2880
2881 for workspace in workspaces.iter() {
2882 db.save_workspace(workspace.clone()).await;
2883 }
2884
2885 let stack = Some(Vec::from([
2886 WindowId::from(2), // Top
2887 WindowId::from(8),
2888 WindowId::from(5),
2889 WindowId::from(9),
2890 WindowId::from(3),
2891 WindowId::from(4), // Bottom
2892 ]));
2893
2894 let locations = db
2895 .last_session_workspace_locations("one-session", stack)
2896 .unwrap();
2897 assert_eq!(
2898 locations,
2899 [
2900 (
2901 SerializedWorkspaceLocation::Local,
2902 PathList::new(&[dir4.path()])
2903 ),
2904 (
2905 SerializedWorkspaceLocation::Local,
2906 PathList::new(&[dir3.path()])
2907 ),
2908 (
2909 SerializedWorkspaceLocation::Local,
2910 PathList::new(&[dir2.path()])
2911 ),
2912 (
2913 SerializedWorkspaceLocation::Local,
2914 PathList::new(&[dir1.path()])
2915 ),
2916 (
2917 SerializedWorkspaceLocation::Local,
2918 PathList::new(&[dir1.path(), dir2.path(), dir3.path()])
2919 ),
2920 (
2921 SerializedWorkspaceLocation::Local,
2922 PathList::new(&[dir4.path(), dir3.path(), dir2.path()])
2923 ),
2924 ]
2925 );
2926 }
2927
2928 #[gpui::test]
2929 async fn test_last_session_workspace_locations_remote() {
2930 let db =
2931 WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces_remote")
2932 .await;
2933
2934 let remote_connections = [
2935 ("host-1", "my-user-1"),
2936 ("host-2", "my-user-2"),
2937 ("host-3", "my-user-3"),
2938 ("host-4", "my-user-4"),
2939 ]
2940 .into_iter()
2941 .map(|(host, user)| async {
2942 let options = RemoteConnectionOptions::Ssh(SshConnectionOptions {
2943 host: host.into(),
2944 username: Some(user.to_string()),
2945 ..Default::default()
2946 });
2947 db.get_or_create_remote_connection(options.clone())
2948 .await
2949 .unwrap();
2950 options
2951 })
2952 .collect::<Vec<_>>();
2953
2954 let remote_connections = futures::future::join_all(remote_connections).await;
2955
2956 let workspaces = [
2957 (1, remote_connections[0].clone(), 9),
2958 (2, remote_connections[1].clone(), 5),
2959 (3, remote_connections[2].clone(), 8),
2960 (4, remote_connections[3].clone(), 2),
2961 ]
2962 .into_iter()
2963 .map(|(id, remote_connection, window_id)| SerializedWorkspace {
2964 id: WorkspaceId(id),
2965 paths: PathList::default(),
2966 location: SerializedWorkspaceLocation::Remote(remote_connection),
2967 center_group: Default::default(),
2968 window_bounds: Default::default(),
2969 display: Default::default(),
2970 docks: Default::default(),
2971 centered_layout: false,
2972 session_id: Some("one-session".to_owned()),
2973 breakpoints: Default::default(),
2974 window_id: Some(window_id),
2975 user_toolchains: Default::default(),
2976 })
2977 .collect::<Vec<_>>();
2978
2979 for workspace in workspaces.iter() {
2980 db.save_workspace(workspace.clone()).await;
2981 }
2982
2983 let stack = Some(Vec::from([
2984 WindowId::from(2), // Top
2985 WindowId::from(8),
2986 WindowId::from(5),
2987 WindowId::from(9), // Bottom
2988 ]));
2989
2990 let have = db
2991 .last_session_workspace_locations("one-session", stack)
2992 .unwrap();
2993 assert_eq!(have.len(), 4);
2994 assert_eq!(
2995 have[0],
2996 (
2997 SerializedWorkspaceLocation::Remote(remote_connections[3].clone()),
2998 PathList::default()
2999 )
3000 );
3001 assert_eq!(
3002 have[1],
3003 (
3004 SerializedWorkspaceLocation::Remote(remote_connections[2].clone()),
3005 PathList::default()
3006 )
3007 );
3008 assert_eq!(
3009 have[2],
3010 (
3011 SerializedWorkspaceLocation::Remote(remote_connections[1].clone()),
3012 PathList::default()
3013 )
3014 );
3015 assert_eq!(
3016 have[3],
3017 (
3018 SerializedWorkspaceLocation::Remote(remote_connections[0].clone()),
3019 PathList::default()
3020 )
3021 );
3022 }
3023
3024 #[gpui::test]
3025 async fn test_get_or_create_ssh_project() {
3026 let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project").await;
3027
3028 let host = "example.com".to_string();
3029 let port = Some(22_u16);
3030 let user = Some("user".to_string());
3031
3032 let connection_id = db
3033 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3034 host: host.clone().into(),
3035 port,
3036 username: user.clone(),
3037 ..Default::default()
3038 }))
3039 .await
3040 .unwrap();
3041
3042 // Test that calling the function again with the same parameters returns the same project
3043 let same_connection = db
3044 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3045 host: host.clone().into(),
3046 port,
3047 username: user.clone(),
3048 ..Default::default()
3049 }))
3050 .await
3051 .unwrap();
3052
3053 assert_eq!(connection_id, same_connection);
3054
3055 // Test with different parameters
3056 let host2 = "otherexample.com".to_string();
3057 let port2 = None;
3058 let user2 = Some("otheruser".to_string());
3059
3060 let different_connection = db
3061 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3062 host: host2.clone().into(),
3063 port: port2,
3064 username: user2.clone(),
3065 ..Default::default()
3066 }))
3067 .await
3068 .unwrap();
3069
3070 assert_ne!(connection_id, different_connection);
3071 }
3072
3073 #[gpui::test]
3074 async fn test_get_or_create_ssh_project_with_null_user() {
3075 let db = WorkspaceDb::open_test_db("test_get_or_create_ssh_project_with_null_user").await;
3076
3077 let (host, port, user) = ("example.com".to_string(), None, None);
3078
3079 let connection_id = db
3080 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3081 host: host.clone().into(),
3082 port,
3083 username: None,
3084 ..Default::default()
3085 }))
3086 .await
3087 .unwrap();
3088
3089 let same_connection_id = db
3090 .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
3091 host: host.clone().into(),
3092 port,
3093 username: user.clone(),
3094 ..Default::default()
3095 }))
3096 .await
3097 .unwrap();
3098
3099 assert_eq!(connection_id, same_connection_id);
3100 }
3101
3102 #[gpui::test]
3103 async fn test_get_remote_connections() {
3104 let db = WorkspaceDb::open_test_db("test_get_remote_connections").await;
3105
3106 let connections = [
3107 ("example.com".to_string(), None, None),
3108 (
3109 "anotherexample.com".to_string(),
3110 Some(123_u16),
3111 Some("user2".to_string()),
3112 ),
3113 ("yetanother.com".to_string(), Some(345_u16), None),
3114 ];
3115
3116 let mut ids = Vec::new();
3117 for (host, port, user) in connections.iter() {
3118 ids.push(
3119 db.get_or_create_remote_connection(RemoteConnectionOptions::Ssh(
3120 SshConnectionOptions {
3121 host: host.clone().into(),
3122 port: *port,
3123 username: user.clone(),
3124 ..Default::default()
3125 },
3126 ))
3127 .await
3128 .unwrap(),
3129 );
3130 }
3131
3132 let stored_connections = db.remote_connections().unwrap();
3133 assert_eq!(
3134 stored_connections,
3135 [
3136 (
3137 ids[0],
3138 RemoteConnectionOptions::Ssh(SshConnectionOptions {
3139 host: "example.com".into(),
3140 port: None,
3141 username: None,
3142 ..Default::default()
3143 }),
3144 ),
3145 (
3146 ids[1],
3147 RemoteConnectionOptions::Ssh(SshConnectionOptions {
3148 host: "anotherexample.com".into(),
3149 port: Some(123),
3150 username: Some("user2".into()),
3151 ..Default::default()
3152 }),
3153 ),
3154 (
3155 ids[2],
3156 RemoteConnectionOptions::Ssh(SshConnectionOptions {
3157 host: "yetanother.com".into(),
3158 port: Some(345),
3159 username: None,
3160 ..Default::default()
3161 }),
3162 ),
3163 ]
3164 .into_iter()
3165 .collect::<HashMap<_, _>>(),
3166 );
3167 }
3168
3169 #[gpui::test]
3170 async fn test_simple_split() {
3171 zlog::init_test();
3172
3173 let db = WorkspaceDb::open_test_db("simple_split").await;
3174
3175 // -----------------
3176 // | 1,2 | 5,6 |
3177 // | - - - | |
3178 // | 3,4 | |
3179 // -----------------
3180 let center_pane = group(
3181 Axis::Horizontal,
3182 vec![
3183 group(
3184 Axis::Vertical,
3185 vec![
3186 SerializedPaneGroup::Pane(SerializedPane::new(
3187 vec![
3188 SerializedItem::new("Terminal", 1, false, false),
3189 SerializedItem::new("Terminal", 2, true, false),
3190 ],
3191 false,
3192 0,
3193 )),
3194 SerializedPaneGroup::Pane(SerializedPane::new(
3195 vec![
3196 SerializedItem::new("Terminal", 4, false, false),
3197 SerializedItem::new("Terminal", 3, true, false),
3198 ],
3199 true,
3200 0,
3201 )),
3202 ],
3203 ),
3204 SerializedPaneGroup::Pane(SerializedPane::new(
3205 vec![
3206 SerializedItem::new("Terminal", 5, true, false),
3207 SerializedItem::new("Terminal", 6, false, false),
3208 ],
3209 false,
3210 0,
3211 )),
3212 ],
3213 );
3214
3215 let workspace = default_workspace(&["/tmp"], ¢er_pane);
3216
3217 db.save_workspace(workspace.clone()).await;
3218
3219 let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap();
3220
3221 assert_eq!(workspace.center_group, new_workspace.center_group);
3222 }
3223
3224 #[gpui::test]
3225 async fn test_cleanup_panes() {
3226 zlog::init_test();
3227
3228 let db = WorkspaceDb::open_test_db("test_cleanup_panes").await;
3229
3230 let center_pane = group(
3231 Axis::Horizontal,
3232 vec![
3233 group(
3234 Axis::Vertical,
3235 vec![
3236 SerializedPaneGroup::Pane(SerializedPane::new(
3237 vec![
3238 SerializedItem::new("Terminal", 1, false, false),
3239 SerializedItem::new("Terminal", 2, true, false),
3240 ],
3241 false,
3242 0,
3243 )),
3244 SerializedPaneGroup::Pane(SerializedPane::new(
3245 vec![
3246 SerializedItem::new("Terminal", 4, false, false),
3247 SerializedItem::new("Terminal", 3, true, false),
3248 ],
3249 true,
3250 0,
3251 )),
3252 ],
3253 ),
3254 SerializedPaneGroup::Pane(SerializedPane::new(
3255 vec![
3256 SerializedItem::new("Terminal", 5, false, false),
3257 SerializedItem::new("Terminal", 6, true, false),
3258 ],
3259 false,
3260 0,
3261 )),
3262 ],
3263 );
3264
3265 let id = &["/tmp"];
3266
3267 let mut workspace = default_workspace(id, ¢er_pane);
3268
3269 db.save_workspace(workspace.clone()).await;
3270
3271 workspace.center_group = group(
3272 Axis::Vertical,
3273 vec![
3274 SerializedPaneGroup::Pane(SerializedPane::new(
3275 vec![
3276 SerializedItem::new("Terminal", 1, false, false),
3277 SerializedItem::new("Terminal", 2, true, false),
3278 ],
3279 false,
3280 0,
3281 )),
3282 SerializedPaneGroup::Pane(SerializedPane::new(
3283 vec![
3284 SerializedItem::new("Terminal", 4, true, false),
3285 SerializedItem::new("Terminal", 3, false, false),
3286 ],
3287 true,
3288 0,
3289 )),
3290 ],
3291 );
3292
3293 db.save_workspace(workspace.clone()).await;
3294
3295 let new_workspace = db.workspace_for_roots(id).unwrap();
3296
3297 assert_eq!(workspace.center_group, new_workspace.center_group);
3298 }
3299}