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