1use super::{SerializedAxis, SerializedWindowBounds};
2use crate::{
3 Member, Pane, PaneAxis, SerializableItemRegistry, Workspace, WorkspaceId, item::ItemHandle,
4};
5use anyhow::{Context as _, Result};
6use async_recursion::async_recursion;
7use db::sqlez::{
8 bindable::{Bind, Column, StaticColumnCount},
9 statement::Statement,
10};
11use gpui::{AsyncWindowContext, Entity, WeakEntity};
12use itertools::Itertools as _;
13use project::{Project, debugger::breakpoint_store::SourceBreakpoint};
14use remote::ssh_session::SshProjectId;
15use serde::{Deserialize, Serialize};
16use std::{
17 collections::BTreeMap,
18 path::{Path, PathBuf},
19 sync::Arc,
20};
21use util::{ResultExt, paths::SanitizedPath};
22use uuid::Uuid;
23
24#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
25pub struct SerializedSshProject {
26 pub id: SshProjectId,
27 pub host: String,
28 pub port: Option<u16>,
29 pub paths: Vec<String>,
30 pub user: Option<String>,
31}
32
33impl SerializedSshProject {
34 pub fn ssh_urls(&self) -> Vec<PathBuf> {
35 self.paths
36 .iter()
37 .map(|path| {
38 let mut result = String::new();
39 if let Some(user) = &self.user {
40 result.push_str(user);
41 result.push('@');
42 }
43 result.push_str(&self.host);
44 if let Some(port) = &self.port {
45 result.push(':');
46 result.push_str(&port.to_string());
47 }
48 result.push_str(path);
49 PathBuf::from(result)
50 })
51 .collect()
52 }
53}
54
55impl StaticColumnCount for SerializedSshProject {
56 fn column_count() -> usize {
57 5
58 }
59}
60
61impl Bind for &SerializedSshProject {
62 fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
63 let next_index = statement.bind(&self.id.0, start_index)?;
64 let next_index = statement.bind(&self.host, next_index)?;
65 let next_index = statement.bind(&self.port, next_index)?;
66 let raw_paths = serde_json::to_string(&self.paths)?;
67 let next_index = statement.bind(&raw_paths, next_index)?;
68 statement.bind(&self.user, next_index)
69 }
70}
71
72impl Column for SerializedSshProject {
73 fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
74 let id = statement.column_int64(start_index)?;
75 let host = statement.column_text(start_index + 1)?.to_string();
76 let (port, _) = Option::<u16>::column(statement, start_index + 2)?;
77 let raw_paths = statement.column_text(start_index + 3)?.to_string();
78 let paths: Vec<String> = serde_json::from_str(&raw_paths)?;
79
80 let (user, _) = Option::<String>::column(statement, start_index + 4)?;
81
82 Ok((
83 Self {
84 id: SshProjectId(id as u64),
85 host,
86 port,
87 paths,
88 user,
89 },
90 start_index + 5,
91 ))
92 }
93}
94
95#[derive(Debug, PartialEq, Clone)]
96pub struct LocalPaths(Arc<Vec<PathBuf>>);
97
98impl LocalPaths {
99 pub fn new<P: AsRef<Path>>(paths: impl IntoIterator<Item = P>) -> Self {
100 let mut paths: Vec<PathBuf> = paths
101 .into_iter()
102 .map(|p| SanitizedPath::from(p).into())
103 .collect();
104 // Ensure all future `zed workspace1 workspace2` and `zed workspace2 workspace1` calls are using the same workspace.
105 // The actual workspace order is stored in the `LocalPathsOrder` struct.
106 paths.sort();
107 Self(Arc::new(paths))
108 }
109
110 pub fn paths(&self) -> &Arc<Vec<PathBuf>> {
111 &self.0
112 }
113}
114
115impl StaticColumnCount for LocalPaths {}
116impl Bind for &LocalPaths {
117 fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
118 statement.bind(&bincode::serialize(&self.0)?, start_index)
119 }
120}
121
122impl Column for LocalPaths {
123 fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
124 let path_blob = statement.column_blob(start_index)?;
125 let paths: Arc<Vec<PathBuf>> = if path_blob.is_empty() {
126 Default::default()
127 } else {
128 bincode::deserialize(path_blob).context("Bincode deserialization of paths failed")?
129 };
130
131 Ok((Self(paths), start_index + 1))
132 }
133}
134
135#[derive(Debug, PartialEq, Clone)]
136pub struct LocalPathsOrder(Vec<usize>);
137
138impl LocalPathsOrder {
139 pub fn new(order: impl IntoIterator<Item = usize>) -> Self {
140 Self(order.into_iter().collect())
141 }
142
143 pub fn order(&self) -> &[usize] {
144 self.0.as_slice()
145 }
146
147 pub fn default_for_paths(paths: &LocalPaths) -> Self {
148 Self::new(0..paths.0.len())
149 }
150}
151
152impl StaticColumnCount for LocalPathsOrder {}
153impl Bind for &LocalPathsOrder {
154 fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
155 statement.bind(&bincode::serialize(&self.0)?, start_index)
156 }
157}
158
159impl Column for LocalPathsOrder {
160 fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
161 let order_blob = statement.column_blob(start_index)?;
162 let order = if order_blob.is_empty() {
163 Vec::new()
164 } else {
165 bincode::deserialize(order_blob).context("deserializing workspace root order")?
166 };
167
168 Ok((Self(order), start_index + 1))
169 }
170}
171
172#[derive(Debug, PartialEq, Clone)]
173pub enum SerializedWorkspaceLocation {
174 Local(LocalPaths, LocalPathsOrder),
175 Ssh(SerializedSshProject),
176}
177
178impl SerializedWorkspaceLocation {
179 /// Create a new `SerializedWorkspaceLocation` from a list of local paths.
180 ///
181 /// The paths will be sorted and the order will be stored in the `LocalPathsOrder` struct.
182 ///
183 /// # Examples
184 ///
185 /// ```
186 /// use std::path::Path;
187 /// use zed_workspace::SerializedWorkspaceLocation;
188 ///
189 /// let location = SerializedWorkspaceLocation::from_local_paths(vec![
190 /// Path::new("path/to/workspace1"),
191 /// Path::new("path/to/workspace2"),
192 /// ]);
193 /// assert_eq!(location, SerializedWorkspaceLocation::Local(
194 /// LocalPaths::new(vec![
195 /// Path::new("path/to/workspace1"),
196 /// Path::new("path/to/workspace2"),
197 /// ]),
198 /// LocalPathsOrder::new(vec![0, 1]),
199 /// ));
200 /// ```
201 ///
202 /// ```
203 /// use std::path::Path;
204 /// use zed_workspace::SerializedWorkspaceLocation;
205 ///
206 /// let location = SerializedWorkspaceLocation::from_local_paths(vec![
207 /// Path::new("path/to/workspace2"),
208 /// Path::new("path/to/workspace1"),
209 /// ]);
210 ///
211 /// assert_eq!(location, SerializedWorkspaceLocation::Local(
212 /// LocalPaths::new(vec![
213 /// Path::new("path/to/workspace1"),
214 /// Path::new("path/to/workspace2"),
215 /// ]),
216 /// LocalPathsOrder::new(vec![1, 0]),
217 /// ));
218 /// ```
219 pub fn from_local_paths<P: AsRef<Path>>(paths: impl IntoIterator<Item = P>) -> Self {
220 let mut indexed_paths: Vec<_> = paths
221 .into_iter()
222 .map(|p| p.as_ref().to_path_buf())
223 .enumerate()
224 .collect();
225
226 indexed_paths.sort_by(|(_, a), (_, b)| a.cmp(b));
227
228 let sorted_paths: Vec<_> = indexed_paths.iter().map(|(_, path)| path.clone()).collect();
229 let order: Vec<_> = indexed_paths.iter().map(|(index, _)| *index).collect();
230
231 Self::Local(LocalPaths::new(sorted_paths), LocalPathsOrder::new(order))
232 }
233
234 /// Get sorted paths
235 pub fn sorted_paths(&self) -> Arc<Vec<PathBuf>> {
236 match self {
237 SerializedWorkspaceLocation::Local(paths, order) => {
238 if order.order().is_empty() {
239 paths.paths().clone()
240 } else {
241 Arc::new(
242 order
243 .order()
244 .iter()
245 .zip(paths.paths().iter())
246 .sorted_by_key(|(i, _)| **i)
247 .map(|(_, p)| p.clone())
248 .collect(),
249 )
250 }
251 }
252 SerializedWorkspaceLocation::Ssh(ssh_project) => Arc::new(ssh_project.ssh_urls()),
253 }
254 }
255}
256
257#[derive(Debug, PartialEq, Clone)]
258pub(crate) struct SerializedWorkspace {
259 pub(crate) id: WorkspaceId,
260 pub(crate) location: SerializedWorkspaceLocation,
261 pub(crate) center_group: SerializedPaneGroup,
262 pub(crate) window_bounds: Option<SerializedWindowBounds>,
263 pub(crate) centered_layout: bool,
264 pub(crate) display: Option<Uuid>,
265 pub(crate) docks: DockStructure,
266 pub(crate) session_id: Option<String>,
267 pub(crate) breakpoints: BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
268 pub(crate) window_id: Option<u64>,
269}
270
271#[derive(Debug, PartialEq, Clone, Default)]
272pub struct DockStructure {
273 pub(crate) left: DockData,
274 pub(crate) right: DockData,
275 pub(crate) bottom: DockData,
276}
277
278impl Column for DockStructure {
279 fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
280 let (left, next_index) = DockData::column(statement, start_index)?;
281 let (right, next_index) = DockData::column(statement, next_index)?;
282 let (bottom, next_index) = DockData::column(statement, next_index)?;
283 Ok((
284 DockStructure {
285 left,
286 right,
287 bottom,
288 },
289 next_index,
290 ))
291 }
292}
293
294impl Bind for DockStructure {
295 fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
296 let next_index = statement.bind(&self.left, start_index)?;
297 let next_index = statement.bind(&self.right, next_index)?;
298 statement.bind(&self.bottom, next_index)
299 }
300}
301
302#[derive(Debug, PartialEq, Clone, Default)]
303pub struct DockData {
304 pub(crate) visible: bool,
305 pub(crate) active_panel: Option<String>,
306 pub(crate) zoom: bool,
307}
308
309impl Column for DockData {
310 fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
311 let (visible, next_index) = Option::<bool>::column(statement, start_index)?;
312 let (active_panel, next_index) = Option::<String>::column(statement, next_index)?;
313 let (zoom, next_index) = Option::<bool>::column(statement, next_index)?;
314 Ok((
315 DockData {
316 visible: visible.unwrap_or(false),
317 active_panel,
318 zoom: zoom.unwrap_or(false),
319 },
320 next_index,
321 ))
322 }
323}
324
325impl Bind for DockData {
326 fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
327 let next_index = statement.bind(&self.visible, start_index)?;
328 let next_index = statement.bind(&self.active_panel, next_index)?;
329 statement.bind(&self.zoom, next_index)
330 }
331}
332
333#[derive(Debug, PartialEq, Clone)]
334pub(crate) enum SerializedPaneGroup {
335 Group {
336 axis: SerializedAxis,
337 flexes: Option<Vec<f32>>,
338 children: Vec<SerializedPaneGroup>,
339 },
340 Pane(SerializedPane),
341}
342
343#[cfg(test)]
344impl Default for SerializedPaneGroup {
345 fn default() -> Self {
346 Self::Pane(SerializedPane {
347 children: vec![SerializedItem::default()],
348 active: false,
349 pinned_count: 0,
350 })
351 }
352}
353
354impl SerializedPaneGroup {
355 #[async_recursion(?Send)]
356 pub(crate) async fn deserialize(
357 self,
358 project: &Entity<Project>,
359 workspace_id: WorkspaceId,
360 workspace: WeakEntity<Workspace>,
361 cx: &mut AsyncWindowContext,
362 ) -> Option<(
363 Member,
364 Option<Entity<Pane>>,
365 Vec<Option<Box<dyn ItemHandle>>>,
366 )> {
367 match self {
368 SerializedPaneGroup::Group {
369 axis,
370 children,
371 flexes,
372 } => {
373 let mut current_active_pane = None;
374 let mut members = Vec::new();
375 let mut items = Vec::new();
376 for child in children {
377 if let Some((new_member, active_pane, new_items)) = child
378 .deserialize(project, workspace_id, workspace.clone(), cx)
379 .await
380 {
381 members.push(new_member);
382 items.extend(new_items);
383 current_active_pane = current_active_pane.or(active_pane);
384 }
385 }
386
387 if members.is_empty() {
388 return None;
389 }
390
391 if members.len() == 1 {
392 return Some((members.remove(0), current_active_pane, items));
393 }
394
395 Some((
396 Member::Axis(PaneAxis::load(axis.0, members, flexes)),
397 current_active_pane,
398 items,
399 ))
400 }
401 SerializedPaneGroup::Pane(serialized_pane) => {
402 let pane = workspace
403 .update_in(cx, |workspace, window, cx| {
404 workspace.add_pane(window, cx).downgrade()
405 })
406 .log_err()?;
407 let active = serialized_pane.active;
408 let new_items = serialized_pane
409 .deserialize_to(project, &pane, workspace_id, workspace.clone(), cx)
410 .await
411 .log_err()?;
412
413 if pane
414 .read_with(cx, |pane, _| pane.items_len() != 0)
415 .log_err()?
416 {
417 let pane = pane.upgrade()?;
418 Some((
419 Member::Pane(pane.clone()),
420 active.then_some(pane),
421 new_items,
422 ))
423 } else {
424 let pane = pane.upgrade()?;
425 workspace
426 .update_in(cx, |workspace, window, cx| {
427 workspace.force_remove_pane(&pane, &None, window, cx)
428 })
429 .log_err()?;
430 None
431 }
432 }
433 }
434 }
435}
436
437#[derive(Debug, PartialEq, Eq, Default, Clone)]
438pub struct SerializedPane {
439 pub(crate) active: bool,
440 pub(crate) children: Vec<SerializedItem>,
441 pub(crate) pinned_count: usize,
442}
443
444impl SerializedPane {
445 pub fn new(children: Vec<SerializedItem>, active: bool, pinned_count: usize) -> Self {
446 SerializedPane {
447 children,
448 active,
449 pinned_count,
450 }
451 }
452
453 pub async fn deserialize_to(
454 &self,
455 project: &Entity<Project>,
456 pane: &WeakEntity<Pane>,
457 workspace_id: WorkspaceId,
458 workspace: WeakEntity<Workspace>,
459 cx: &mut AsyncWindowContext,
460 ) -> Result<Vec<Option<Box<dyn ItemHandle>>>> {
461 let mut item_tasks = Vec::new();
462 let mut active_item_index = None;
463 let mut preview_item_index = None;
464 for (index, item) in self.children.iter().enumerate() {
465 let project = project.clone();
466 item_tasks.push(pane.update_in(cx, |_, window, cx| {
467 SerializableItemRegistry::deserialize(
468 &item.kind,
469 project,
470 workspace.clone(),
471 workspace_id,
472 item.item_id,
473 window,
474 cx,
475 )
476 })?);
477 if item.active {
478 active_item_index = Some(index);
479 }
480 if item.preview {
481 preview_item_index = Some(index);
482 }
483 }
484
485 let mut items = Vec::new();
486 for item_handle in futures::future::join_all(item_tasks).await {
487 let item_handle = item_handle.log_err();
488 items.push(item_handle.clone());
489
490 if let Some(item_handle) = item_handle {
491 pane.update_in(cx, |pane, window, cx| {
492 pane.add_item(item_handle.clone(), true, true, None, window, cx);
493 })?;
494 }
495 }
496
497 if let Some(active_item_index) = active_item_index {
498 pane.update_in(cx, |pane, window, cx| {
499 pane.activate_item(active_item_index, false, false, window, cx);
500 })?;
501 }
502
503 if let Some(preview_item_index) = preview_item_index {
504 pane.update(cx, |pane, cx| {
505 if let Some(item) = pane.item_for_index(preview_item_index) {
506 pane.set_preview_item_id(Some(item.item_id()), cx);
507 }
508 })?;
509 }
510 pane.update(cx, |pane, _| {
511 pane.set_pinned_count(self.pinned_count.min(items.len()));
512 })?;
513
514 anyhow::Ok(items)
515 }
516}
517
518pub type GroupId = i64;
519pub type PaneId = i64;
520pub type ItemId = u64;
521
522#[derive(Debug, PartialEq, Eq, Clone)]
523pub struct SerializedItem {
524 pub kind: Arc<str>,
525 pub item_id: ItemId,
526 pub active: bool,
527 pub preview: bool,
528}
529
530impl SerializedItem {
531 pub fn new(kind: impl AsRef<str>, item_id: ItemId, active: bool, preview: bool) -> Self {
532 Self {
533 kind: Arc::from(kind.as_ref()),
534 item_id,
535 active,
536 preview,
537 }
538 }
539}
540
541#[cfg(test)]
542impl Default for SerializedItem {
543 fn default() -> Self {
544 SerializedItem {
545 kind: Arc::from("Terminal"),
546 item_id: 100000,
547 active: false,
548 preview: false,
549 }
550 }
551}
552
553impl StaticColumnCount for SerializedItem {
554 fn column_count() -> usize {
555 4
556 }
557}
558impl Bind for &SerializedItem {
559 fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
560 let next_index = statement.bind(&self.kind, start_index)?;
561 let next_index = statement.bind(&self.item_id, next_index)?;
562 let next_index = statement.bind(&self.active, next_index)?;
563 statement.bind(&self.preview, next_index)
564 }
565}
566
567impl Column for SerializedItem {
568 fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
569 let (kind, next_index) = Arc::<str>::column(statement, start_index)?;
570 let (item_id, next_index) = ItemId::column(statement, next_index)?;
571 let (active, next_index) = bool::column(statement, next_index)?;
572 let (preview, next_index) = bool::column(statement, next_index)?;
573 Ok((
574 SerializedItem {
575 kind,
576 item_id,
577 active,
578 preview,
579 },
580 next_index,
581 ))
582 }
583}
584
585#[cfg(test)]
586mod tests {
587 use super::*;
588
589 #[test]
590 fn test_serialize_local_paths() {
591 let paths = vec!["b", "a", "c"];
592 let serialized = SerializedWorkspaceLocation::from_local_paths(paths);
593
594 assert_eq!(
595 serialized,
596 SerializedWorkspaceLocation::Local(
597 LocalPaths::new(vec!["a", "b", "c"]),
598 LocalPathsOrder::new(vec![1, 0, 2])
599 )
600 );
601 }
602
603 #[test]
604 fn test_sorted_paths() {
605 let paths = vec!["b", "a", "c"];
606 let serialized = SerializedWorkspaceLocation::from_local_paths(paths);
607 assert_eq!(
608 serialized.sorted_paths(),
609 Arc::new(vec![
610 PathBuf::from("b"),
611 PathBuf::from("a"),
612 PathBuf::from("c"),
613 ])
614 );
615
616 let paths = Arc::new(vec![
617 PathBuf::from("a"),
618 PathBuf::from("b"),
619 PathBuf::from("c"),
620 ]);
621 let order = vec![2, 0, 1];
622 let serialized =
623 SerializedWorkspaceLocation::Local(LocalPaths(paths), LocalPathsOrder(order));
624 assert_eq!(
625 serialized.sorted_paths(),
626 Arc::new(vec![
627 PathBuf::from("b"),
628 PathBuf::from("c"),
629 PathBuf::from("a"),
630 ])
631 );
632
633 let paths = Arc::new(vec![
634 PathBuf::from("a"),
635 PathBuf::from("b"),
636 PathBuf::from("c"),
637 ]);
638 let order = vec![];
639 let serialized =
640 SerializedWorkspaceLocation::Local(LocalPaths(paths.clone()), LocalPathsOrder(order));
641 assert_eq!(serialized.sorted_paths(), paths);
642
643 let urls = ["/a", "/b", "/c"];
644 let serialized = SerializedWorkspaceLocation::Ssh(SerializedSshProject {
645 id: SshProjectId(0),
646 host: "host".to_string(),
647 port: Some(22),
648 paths: urls.iter().map(|s| s.to_string()).collect(),
649 user: Some("user".to_string()),
650 });
651 assert_eq!(
652 serialized.sorted_paths(),
653 Arc::new(
654 urls.iter()
655 .map(|p| PathBuf::from(format!("user@host:22{}", p)))
656 .collect()
657 )
658 );
659 }
660}