Fix empty title in Recent Projects (#21952)

Jason Lee created

Close #13595 

Release Notes:

- Fixed empty title in Recent Projects.

---

| Before | After |
| --- | --- |
| <img width="695" alt="SCR-20241213-nzxr"
src="https://github.com/user-attachments/assets/f19a0bad-d542-44cd-85c1-89386d396f27"
/> | <img width="625" alt="image"
src="https://github.com/user-attachments/assets/0d2afef7-4cd2-43eb-9046-c169df2eb8a0"
/> |

This is because the `LocalPathsOrder` get empty list.

```
[crates/recent_projects/src/recent_projects.rs:385:9] &location = Local(
    LocalPaths(
        [
            "/Users/jason/Library/Application Support/Zed/prettier/node_modules",
        ],
    ),
    LocalPathsOrder(
        [],
    ),
)
[crates/recent_projects/src/recent_projects.rs:386:9] &paths = [
    "~/Library/Application Support/Zed/prettier/node_modules",
]
[crates/recent_projects/src/recent_projects.rs:385:9] &location = Local(
    LocalPaths(
        [
            "/Users/jason/github/tree-sitter-csv",
        ],
    ),
    LocalPathsOrder(
        [],
    ),
)
[crates/recent_projects/src/recent_projects.rs:386:9] &paths = [
    "~/github/tree-sitter-csv",
]
[crates/recent_projects/src/recent_projects.rs:385:9] &location = Local(
    LocalPaths(
        [
            "/Users/jason/work/autocorrect/autocorrect-website/dist",
        ],
    ),
    LocalPathsOrder(
        [],
    ),
)
```

Change summary

Cargo.lock                                    |  1 
crates/recent_projects/Cargo.toml             |  1 
crates/recent_projects/src/recent_projects.rs | 39 ++-------
crates/workspace/src/persistence/model.rs     | 81 +++++++++++++++++++++
crates/workspace/src/workspace.rs             | 14 --
5 files changed, 93 insertions(+), 43 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -10340,7 +10340,6 @@ dependencies = [
  "futures 0.3.31",
  "fuzzy",
  "gpui",
- "itertools 0.13.0",
  "language",
  "log",
  "markdown",

crates/recent_projects/Cargo.toml 🔗

@@ -22,7 +22,6 @@ file_finder.workspace = true
 futures.workspace = true
 fuzzy.workspace = true
 gpui.workspace = true
-itertools.workspace = true
 log.workspace = true
 language.workspace = true
 markdown.workspace = true

crates/recent_projects/src/recent_projects.rs 🔗

@@ -9,7 +9,6 @@ use gpui::{
     Action, AnyElement, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView,
     Subscription, Task, View, ViewContext, WeakView,
 };
-use itertools::Itertools;
 use ordered_float::OrderedFloat;
 use picker::{
     highlighted_match_with_paths::{HighlightedMatchWithPaths, HighlightedText},
@@ -211,22 +210,12 @@ impl PickerDelegate for RecentProjectsDelegate {
             .enumerate()
             .filter(|(_, (id, _))| !self.is_current_workspace(*id, cx))
             .map(|(id, (_, location))| {
-                let combined_string = match location {
-                    SerializedWorkspaceLocation::Local(paths, order) => order
-                        .order()
-                        .iter()
-                        .zip(paths.paths().iter())
-                        .sorted_by_key(|(i, _)| *i)
-                        .map(|(_, path)| path.compact().to_string_lossy().into_owned())
-                        .collect::<Vec<_>>()
-                        .join(""),
-                    SerializedWorkspaceLocation::Ssh(ssh_project) => ssh_project
-                        .ssh_urls()
-                        .iter()
-                        .map(|path| path.to_string_lossy().to_string())
-                        .collect::<Vec<_>>()
-                        .join(""),
-                };
+                let combined_string = location
+                    .sorted_paths()
+                    .iter()
+                    .map(|path| path.compact().to_string_lossy().into_owned())
+                    .collect::<Vec<_>>()
+                    .join("");
 
                 StringMatchCandidate::new(id, &combined_string)
             })
@@ -364,21 +353,11 @@ impl PickerDelegate for RecentProjectsDelegate {
         let (_, location) = self.workspaces.get(hit.candidate_id)?;
 
         let mut path_start_offset = 0;
-        let paths = match location {
-            SerializedWorkspaceLocation::Local(paths, order) => Arc::new(
-                order
-                    .order()
-                    .iter()
-                    .zip(paths.paths().iter())
-                    .sorted_by_key(|(i, _)| **i)
-                    .map(|(_, path)| path.compact())
-                    .collect(),
-            ),
-            SerializedWorkspaceLocation::Ssh(ssh_project) => Arc::new(ssh_project.ssh_urls()),
-        };
 
-        let (match_labels, paths): (Vec<_>, Vec<_>) = paths
+        let (match_labels, paths): (Vec<_>, Vec<_>) = location
+            .sorted_paths()
             .iter()
+            .map(|p| p.compact())
             .map(|path| {
                 let highlighted_text =
                     highlights_for_path(path.as_ref(), &hit.positions, path_start_offset);

crates/workspace/src/persistence/model.rs 🔗

@@ -9,6 +9,7 @@ use db::sqlez::{
     statement::Statement,
 };
 use gpui::{AsyncWindowContext, Model, View, WeakView};
+use itertools::Itertools as _;
 use project::Project;
 use remote::ssh_session::SshProjectId;
 use serde::{Deserialize, Serialize};
@@ -228,6 +229,28 @@ impl SerializedWorkspaceLocation {
 
         Self::Local(LocalPaths::new(sorted_paths), LocalPathsOrder::new(order))
     }
+
+    /// Get sorted paths
+    pub fn sorted_paths(&self) -> Arc<Vec<PathBuf>> {
+        match self {
+            SerializedWorkspaceLocation::Local(paths, order) => {
+                if order.order().len() == 0 {
+                    paths.paths().clone()
+                } else {
+                    Arc::new(
+                        order
+                            .order()
+                            .iter()
+                            .zip(paths.paths().iter())
+                            .sorted_by_key(|(i, _)| **i)
+                            .map(|(_, p)| p.clone())
+                            .collect(),
+                    )
+                }
+            }
+            SerializedWorkspaceLocation::Ssh(ssh_project) => Arc::new(ssh_project.ssh_urls()),
+        }
+    }
 }
 
 #[derive(Debug, PartialEq, Clone)]
@@ -564,4 +587,62 @@ mod tests {
             )
         );
     }
+
+    #[test]
+    fn test_sorted_paths() {
+        let paths = vec!["b", "a", "c"];
+        let serialized = SerializedWorkspaceLocation::from_local_paths(paths);
+        assert_eq!(
+            serialized.sorted_paths(),
+            Arc::new(vec![
+                PathBuf::from("b"),
+                PathBuf::from("a"),
+                PathBuf::from("c"),
+            ])
+        );
+
+        let paths = Arc::new(vec![
+            PathBuf::from("a"),
+            PathBuf::from("b"),
+            PathBuf::from("c"),
+        ]);
+        let order = vec![2, 0, 1];
+        let serialized =
+            SerializedWorkspaceLocation::Local(LocalPaths(paths.clone()), LocalPathsOrder(order));
+        assert_eq!(
+            serialized.sorted_paths(),
+            Arc::new(vec![
+                PathBuf::from("b"),
+                PathBuf::from("c"),
+                PathBuf::from("a"),
+            ])
+        );
+
+        let paths = Arc::new(vec![
+            PathBuf::from("a"),
+            PathBuf::from("b"),
+            PathBuf::from("c"),
+        ]);
+        let order = vec![];
+        let serialized =
+            SerializedWorkspaceLocation::Local(LocalPaths(paths.clone()), LocalPathsOrder(order));
+        assert_eq!(serialized.sorted_paths(), paths);
+
+        let urls = ["/a", "/b", "/c"];
+        let serialized = SerializedWorkspaceLocation::Ssh(SerializedSshProject {
+            id: SshProjectId(0),
+            host: "host".to_string(),
+            port: Some(22),
+            paths: urls.iter().map(|s| s.to_string()).collect(),
+            user: Some("user".to_string()),
+        });
+        assert_eq!(
+            serialized.sorted_paths(),
+            Arc::new(
+                urls.iter()
+                    .map(|p| PathBuf::from(format!("user@host:22{}", p)))
+                    .collect()
+            )
+        );
+    }
 }

crates/workspace/src/workspace.rs 🔗

@@ -1127,22 +1127,14 @@ impl Workspace {
                 .as_ref()
                 .map(|ws| &ws.location)
                 .and_then(|loc| match loc {
-                    SerializedWorkspaceLocation::Local(paths, order) => {
-                        Some((paths.paths(), order.order()))
+                    SerializedWorkspaceLocation::Local(_, order) => {
+                        Some((loc.sorted_paths(), order.order()))
                     }
                     _ => None,
                 });
 
             if let Some((paths, order)) = workspace_location {
-                // todo: should probably move this logic to a method on the SerializedWorkspaceLocation
-                // it's only valid for Local and would be more clear there and be able to be tested
-                // and reused elsewhere
-                paths_to_open = order
-                    .iter()
-                    .zip(paths.iter())
-                    .sorted_by_key(|(i, _)| *i)
-                    .map(|(_, path)| path.clone())
-                    .collect();
+                paths_to_open = paths.iter().cloned().collect();
 
                 if order.iter().enumerate().any(|(i, &j)| i != j) {
                     project_handle