model.rs

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