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().len() == 0 {
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.update(cx, |pane, _| pane.items_len() != 0).log_err()? {
414 let pane = pane.upgrade()?;
415 Some((
416 Member::Pane(pane.clone()),
417 active.then_some(pane),
418 new_items,
419 ))
420 } else {
421 let pane = pane.upgrade()?;
422 workspace
423 .update_in(cx, |workspace, window, cx| {
424 workspace.force_remove_pane(&pane, &None, window, cx)
425 })
426 .log_err()?;
427 None
428 }
429 }
430 }
431 }
432}
433
434#[derive(Debug, PartialEq, Eq, Default, Clone)]
435pub struct SerializedPane {
436 pub(crate) active: bool,
437 pub(crate) children: Vec<SerializedItem>,
438 pub(crate) pinned_count: usize,
439}
440
441impl SerializedPane {
442 pub fn new(children: Vec<SerializedItem>, active: bool, pinned_count: usize) -> Self {
443 SerializedPane {
444 children,
445 active,
446 pinned_count,
447 }
448 }
449
450 pub async fn deserialize_to(
451 &self,
452 project: &Entity<Project>,
453 pane: &WeakEntity<Pane>,
454 workspace_id: WorkspaceId,
455 workspace: WeakEntity<Workspace>,
456 cx: &mut AsyncWindowContext,
457 ) -> Result<Vec<Option<Box<dyn ItemHandle>>>> {
458 let mut item_tasks = Vec::new();
459 let mut active_item_index = None;
460 let mut preview_item_index = None;
461 for (index, item) in self.children.iter().enumerate() {
462 let project = project.clone();
463 item_tasks.push(pane.update_in(cx, |_, window, cx| {
464 SerializableItemRegistry::deserialize(
465 &item.kind,
466 project,
467 workspace.clone(),
468 workspace_id,
469 item.item_id,
470 window,
471 cx,
472 )
473 })?);
474 if item.active {
475 active_item_index = Some(index);
476 }
477 if item.preview {
478 preview_item_index = Some(index);
479 }
480 }
481
482 let mut items = Vec::new();
483 for item_handle in futures::future::join_all(item_tasks).await {
484 let item_handle = item_handle.log_err();
485 items.push(item_handle.clone());
486
487 if let Some(item_handle) = item_handle {
488 pane.update_in(cx, |pane, window, cx| {
489 pane.add_item(item_handle.clone(), true, true, None, window, cx);
490 })?;
491 }
492 }
493
494 if let Some(active_item_index) = active_item_index {
495 pane.update_in(cx, |pane, window, cx| {
496 pane.activate_item(active_item_index, false, false, window, cx);
497 })?;
498 }
499
500 if let Some(preview_item_index) = preview_item_index {
501 pane.update(cx, |pane, cx| {
502 if let Some(item) = pane.item_for_index(preview_item_index) {
503 pane.set_preview_item_id(Some(item.item_id()), cx);
504 }
505 })?;
506 }
507 pane.update(cx, |pane, _| {
508 pane.set_pinned_count(self.pinned_count.min(items.len()));
509 })?;
510
511 anyhow::Ok(items)
512 }
513}
514
515pub type GroupId = i64;
516pub type PaneId = i64;
517pub type ItemId = u64;
518
519#[derive(Debug, PartialEq, Eq, Clone)]
520pub struct SerializedItem {
521 pub kind: Arc<str>,
522 pub item_id: ItemId,
523 pub active: bool,
524 pub preview: bool,
525}
526
527impl SerializedItem {
528 pub fn new(kind: impl AsRef<str>, item_id: ItemId, active: bool, preview: bool) -> Self {
529 Self {
530 kind: Arc::from(kind.as_ref()),
531 item_id,
532 active,
533 preview,
534 }
535 }
536}
537
538#[cfg(test)]
539impl Default for SerializedItem {
540 fn default() -> Self {
541 SerializedItem {
542 kind: Arc::from("Terminal"),
543 item_id: 100000,
544 active: false,
545 preview: false,
546 }
547 }
548}
549
550impl StaticColumnCount for SerializedItem {
551 fn column_count() -> usize {
552 4
553 }
554}
555impl Bind for &SerializedItem {
556 fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
557 let next_index = statement.bind(&self.kind, start_index)?;
558 let next_index = statement.bind(&self.item_id, next_index)?;
559 let next_index = statement.bind(&self.active, next_index)?;
560 statement.bind(&self.preview, next_index)
561 }
562}
563
564impl Column for SerializedItem {
565 fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
566 let (kind, next_index) = Arc::<str>::column(statement, start_index)?;
567 let (item_id, next_index) = ItemId::column(statement, next_index)?;
568 let (active, next_index) = bool::column(statement, next_index)?;
569 let (preview, next_index) = bool::column(statement, next_index)?;
570 Ok((
571 SerializedItem {
572 kind,
573 item_id,
574 active,
575 preview,
576 },
577 next_index,
578 ))
579 }
580}
581
582#[cfg(test)]
583mod tests {
584 use super::*;
585
586 #[test]
587 fn test_serialize_local_paths() {
588 let paths = vec!["b", "a", "c"];
589 let serialized = SerializedWorkspaceLocation::from_local_paths(paths);
590
591 assert_eq!(
592 serialized,
593 SerializedWorkspaceLocation::Local(
594 LocalPaths::new(vec!["a", "b", "c"]),
595 LocalPathsOrder::new(vec![1, 0, 2])
596 )
597 );
598 }
599
600 #[test]
601 fn test_sorted_paths() {
602 let paths = vec!["b", "a", "c"];
603 let serialized = SerializedWorkspaceLocation::from_local_paths(paths);
604 assert_eq!(
605 serialized.sorted_paths(),
606 Arc::new(vec![
607 PathBuf::from("b"),
608 PathBuf::from("a"),
609 PathBuf::from("c"),
610 ])
611 );
612
613 let paths = Arc::new(vec![
614 PathBuf::from("a"),
615 PathBuf::from("b"),
616 PathBuf::from("c"),
617 ]);
618 let order = vec![2, 0, 1];
619 let serialized =
620 SerializedWorkspaceLocation::Local(LocalPaths(paths.clone()), LocalPathsOrder(order));
621 assert_eq!(
622 serialized.sorted_paths(),
623 Arc::new(vec![
624 PathBuf::from("b"),
625 PathBuf::from("c"),
626 PathBuf::from("a"),
627 ])
628 );
629
630 let paths = Arc::new(vec![
631 PathBuf::from("a"),
632 PathBuf::from("b"),
633 PathBuf::from("c"),
634 ]);
635 let order = vec![];
636 let serialized =
637 SerializedWorkspaceLocation::Local(LocalPaths(paths.clone()), LocalPathsOrder(order));
638 assert_eq!(serialized.sorted_paths(), paths);
639
640 let urls = ["/a", "/b", "/c"];
641 let serialized = SerializedWorkspaceLocation::Ssh(SerializedSshProject {
642 id: SshProjectId(0),
643 host: "host".to_string(),
644 port: Some(22),
645 paths: urls.iter().map(|s| s.to_string()).collect(),
646 user: Some("user".to_string()),
647 });
648 assert_eq!(
649 serialized.sorted_paths(),
650 Arc::new(
651 urls.iter()
652 .map(|p| PathBuf::from(format!("user@host:22{}", p)))
653 .collect()
654 )
655 );
656 }
657}