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