collab: Fix project sharing between Windows and Unix (#23680)

张小白 created

Closes #14258

Windows user(host) sharing a project to a guest(using macOS), and host
follows guest:


https://github.com/user-attachments/assets/ba306b6b-23f7-48b1-8ba8-fdc5992d8f00

macOS user(host) sharing a project to a guest(using Windows), and host
follows guest:



https://github.com/user-attachments/assets/c5ee5e78-870d-49e5-907d-8565977a01ae

macOS user edits files in a windows project through collab:



https://github.com/user-attachments/assets/581057cf-e7df-4e56-a0ce-ced74339906a





Release Notes:

- N/A

Change summary

crates/file_finder/src/open_path_prompt.rs       |  2 
crates/git/src/repository.rs                     |  4 
crates/project/src/buffer_store.rs               |  8 +
crates/project/src/git.rs                        |  5 
crates/project/src/lsp_store.rs                  | 25 +++--
crates/project/src/project.rs                    | 16 ++-
crates/project/src/project_settings.rs           | 19 ++--
crates/project/src/toolchain_store.rs            | 50 ++++++++---
crates/project/src/worktree_store.rs             | 20 ++--
crates/proto/proto/zed.proto                     |  3 
crates/proto/src/proto.rs                        | 76 ++++++++++++++++++
crates/remote_server/src/headless_project.rs     | 19 ++--
crates/remote_server/src/remote_editing_tests.rs | 20 ++--
crates/worktree/src/worktree.rs                  | 76 +++++++++--------
14 files changed, 226 insertions(+), 117 deletions(-)

Detailed changes

crates/file_finder/src/open_path_prompt.rs 🔗

@@ -93,6 +93,8 @@ impl PickerDelegate for OpenPathDelegate {
         cx.notify();
     }
 
+    // todo(windows)
+    // Is this method woring correctly on Windows? This method uses `/` for path separator.
     fn update_matches(
         &mut self,
         query: String,

crates/git/src/repository.rs 🔗

@@ -571,10 +571,6 @@ impl RepoPath {
 
         RepoPath(path.into())
     }
-
-    pub fn to_proto(&self) -> String {
-        self.0.to_string_lossy().to_string()
-    }
 }
 
 impl std::fmt::Display for RepoPath {

crates/project/src/buffer_store.rs 🔗

@@ -23,7 +23,10 @@ use language::{
     },
     Buffer, BufferEvent, Capability, DiskState, File as _, Language, LanguageRegistry, Operation,
 };
-use rpc::{proto, AnyProtoClient, ErrorExt as _, TypedEnvelope};
+use rpc::{
+    proto::{self, ToProto},
+    AnyProtoClient, ErrorExt as _, TypedEnvelope,
+};
 use serde::Deserialize;
 use smol::channel::Receiver;
 use std::{
@@ -580,13 +583,12 @@ impl RemoteBufferStore {
         let worktree_id = worktree.read(cx).id().to_proto();
         let project_id = self.project_id;
         let client = self.upstream_client.clone();
-        let path_string = path.clone().to_string_lossy().to_string();
         cx.spawn(move |this, mut cx| async move {
             let response = client
                 .request(proto::OpenBufferByPath {
                     project_id,
                     worktree_id,
-                    path: path_string,
+                    path: path.to_proto(),
                 })
                 .await?;
             let buffer_id = BufferId::new(response.buffer_id)?;

crates/project/src/git.rs 🔗

@@ -13,6 +13,7 @@ use gpui::{
     App, AppContext, Context, Entity, EventEmitter, SharedString, Subscription, Task, WeakEntity,
 };
 use language::{Buffer, LanguageRegistry};
+use rpc::proto::ToProto;
 use rpc::{proto, AnyProtoClient};
 use settings::WorktreeId;
 use std::path::{Path, PathBuf};
@@ -222,7 +223,7 @@ impl GitState {
                                 work_directory_id: work_directory_id.to_proto(),
                                 paths: paths
                                     .into_iter()
-                                    .map(|repo_path| repo_path.to_proto())
+                                    .map(|repo_path| repo_path.as_ref().to_proto())
                                     .collect(),
                             })
                             .await
@@ -247,7 +248,7 @@ impl GitState {
                                 work_directory_id: work_directory_id.to_proto(),
                                 paths: paths
                                     .into_iter()
-                                    .map(|repo_path| repo_path.to_proto())
+                                    .map(|repo_path| repo_path.as_ref().to_proto())
                                     .collect(),
                             })
                             .await

crates/project/src/lsp_store.rs 🔗

@@ -55,7 +55,10 @@ use parking_lot::Mutex;
 use postage::watch;
 use rand::prelude::*;
 
-use rpc::AnyProtoClient;
+use rpc::{
+    proto::{FromProto, ToProto},
+    AnyProtoClient,
+};
 use serde::Serialize;
 use settings::{Settings, SettingsLocation, SettingsStore};
 use sha2::{Digest, Sha256};
@@ -5360,7 +5363,7 @@ impl LspStore {
                         project_id: *project_id,
                         worktree_id: worktree_id.to_proto(),
                         summary: Some(proto::DiagnosticSummary {
-                            path: worktree_path.to_string_lossy().to_string(),
+                            path: worktree_path.to_proto(),
                             language_server_id: server_id.0 as u64,
                             error_count: new_summary.error_count as u32,
                             warning_count: new_summary.warning_count as u32,
@@ -5848,10 +5851,8 @@ impl LspStore {
             .ok_or_else(|| anyhow!("worktree not found"))?;
         let (old_abs_path, new_abs_path) = {
             let root_path = worktree.update(&mut cx, |this, _| this.abs_path())?;
-            (
-                root_path.join(&old_path),
-                root_path.join(&envelope.payload.new_path),
-            )
+            let new_path = PathBuf::from_proto(envelope.payload.new_path.clone());
+            (root_path.join(&old_path), root_path.join(&new_path))
         };
 
         Self::will_rename_entry(
@@ -5881,7 +5882,7 @@ impl LspStore {
             if let Some(message) = envelope.payload.summary {
                 let project_path = ProjectPath {
                     worktree_id,
-                    path: Path::new(&message.path).into(),
+                    path: Arc::<Path>::from_proto(message.path),
                 };
                 let path = project_path.path.clone();
                 let server_id = LanguageServerId(message.language_server_id as usize);
@@ -5915,7 +5916,7 @@ impl LspStore {
                             project_id: *project_id,
                             worktree_id: worktree_id.to_proto(),
                             summary: Some(proto::DiagnosticSummary {
-                                path: project_path.path.to_string_lossy().to_string(),
+                                path: project_path.path.as_ref().to_proto(),
                                 language_server_id: server_id.0 as u64,
                                 error_count: summary.error_count as u32,
                                 warning_count: summary.warning_count as u32,
@@ -7114,7 +7115,7 @@ impl LspStore {
                                 project_id,
                                 worktree_id: worktree_id.to_proto(),
                                 summary: Some(proto::DiagnosticSummary {
-                                    path: path.to_string_lossy().to_string(),
+                                    path: path.as_ref().to_proto(),
                                     language_server_id: server_id.0 as u64,
                                     error_count: 0,
                                     warning_count: 0,
@@ -7768,7 +7769,7 @@ impl LspStore {
             language_server_name: symbol.language_server_name.0.to_string(),
             source_worktree_id: symbol.source_worktree_id.to_proto(),
             worktree_id: symbol.path.worktree_id.to_proto(),
-            path: symbol.path.path.to_string_lossy().to_string(),
+            path: symbol.path.path.as_ref().to_proto(),
             name: symbol.name.clone(),
             kind: unsafe { mem::transmute::<lsp::SymbolKind, i32>(symbol.kind) },
             start: Some(proto::PointUtf16 {
@@ -7789,7 +7790,7 @@ impl LspStore {
         let kind = unsafe { mem::transmute::<i32, lsp::SymbolKind>(serialized_symbol.kind) };
         let path = ProjectPath {
             worktree_id,
-            path: PathBuf::from(serialized_symbol.path).into(),
+            path: Arc::<Path>::from_proto(serialized_symbol.path),
         };
 
         let start = serialized_symbol
@@ -8263,7 +8264,7 @@ impl DiagnosticSummary {
         path: &Path,
     ) -> proto::DiagnosticSummary {
         proto::DiagnosticSummary {
-            path: path.to_string_lossy().to_string(),
+            path: path.to_proto(),
             language_server_id: language_server_id.0 as u64,
             error_count: self.error_count as u32,
             warning_count: self.warning_count as u32,

crates/project/src/project.rs 🔗

@@ -73,7 +73,7 @@ pub use prettier_store::PrettierStore;
 use project_settings::{ProjectSettings, SettingsObserver, SettingsObserverEvent};
 use remote::{SshConnectionOptions, SshRemoteClient};
 use rpc::{
-    proto::{LanguageServerPromptResponse, SSH_PROJECT_ID},
+    proto::{FromProto, LanguageServerPromptResponse, ToProto, SSH_PROJECT_ID},
     AnyProtoClient, ErrorCode,
 };
 use search::{SearchInputKind, SearchQuery, SearchResult};
@@ -297,14 +297,14 @@ impl ProjectPath {
     pub fn from_proto(p: proto::ProjectPath) -> Self {
         Self {
             worktree_id: WorktreeId::from_proto(p.worktree_id),
-            path: Arc::from(PathBuf::from(p.path)),
+            path: Arc::<Path>::from_proto(p.path),
         }
     }
 
     pub fn to_proto(&self) -> proto::ProjectPath {
         proto::ProjectPath {
             worktree_id: self.worktree_id.to_proto(),
-            path: self.path.to_string_lossy().to_string(),
+            path: self.path.as_ref().to_proto(),
         }
     }
 
@@ -3360,18 +3360,19 @@ impl Project {
                 })
             })
         } else if let Some(ssh_client) = self.ssh_client.as_ref() {
+            let request_path = Path::new(path);
             let request = ssh_client
                 .read(cx)
                 .proto_client()
                 .request(proto::GetPathMetadata {
                     project_id: SSH_PROJECT_ID,
-                    path: path.to_string(),
+                    path: request_path.to_proto(),
                 });
             cx.background_executor().spawn(async move {
                 let response = request.await.log_err()?;
                 if response.exists {
                     Some(ResolvedPath::AbsPath {
-                        path: PathBuf::from(response.path),
+                        path: PathBuf::from_proto(response.path),
                         is_dir: response.is_dir,
                     })
                 } else {
@@ -3441,9 +3442,10 @@ impl Project {
         if self.is_local() {
             DirectoryLister::Local(self.fs.clone()).list_directory(query, cx)
         } else if let Some(session) = self.ssh_client.as_ref() {
+            let path_buf = PathBuf::from(query);
             let request = proto::ListRemoteDirectory {
                 dev_server_id: SSH_PROJECT_ID,
-                path: query,
+                path: path_buf.to_proto(),
             };
 
             let response = session.read(cx).proto_client().request(request);
@@ -3994,7 +3996,7 @@ impl Project {
             this.open_buffer(
                 ProjectPath {
                     worktree_id,
-                    path: PathBuf::from(envelope.payload.path).into(),
+                    path: Arc::<Path>::from_proto(envelope.payload.path),
                 },
                 cx,
             )

crates/project/src/project_settings.rs 🔗

@@ -7,18 +7,17 @@ use paths::{
     local_settings_file_relative_path, local_tasks_file_relative_path,
     local_vscode_tasks_file_relative_path, EDITORCONFIG_NAME,
 };
-use rpc::{proto, AnyProtoClient, TypedEnvelope};
+use rpc::{
+    proto::{self, FromProto, ToProto},
+    AnyProtoClient, TypedEnvelope,
+};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use settings::{
     parse_json_with_comments, InvalidSettingsError, LocalSettingsKind, Settings, SettingsLocation,
     SettingsSources, SettingsStore,
 };
-use std::{
-    path::{Path, PathBuf},
-    sync::Arc,
-    time::Duration,
-};
+use std::{path::Path, sync::Arc, time::Duration};
 use task::{TaskTemplates, VsCodeTaskFile};
 use util::ResultExt;
 use worktree::{PathChange, UpdatedEntriesSet, Worktree, WorktreeId};
@@ -292,7 +291,7 @@ impl SettingsObserver {
                     .send(proto::UpdateWorktreeSettings {
                         project_id,
                         worktree_id,
-                        path: path.to_string_lossy().into(),
+                        path: path.to_proto(),
                         content: Some(content),
                         kind: Some(
                             local_settings_kind_to_proto(LocalSettingsKind::Settings).into(),
@@ -305,7 +304,7 @@ impl SettingsObserver {
                     .send(proto::UpdateWorktreeSettings {
                         project_id,
                         worktree_id,
-                        path: path.to_string_lossy().into(),
+                        path: path.to_proto(),
                         content: Some(content),
                         kind: Some(
                             local_settings_kind_to_proto(LocalSettingsKind::Editorconfig).into(),
@@ -343,7 +342,7 @@ impl SettingsObserver {
             this.update_settings(
                 worktree,
                 [(
-                    PathBuf::from(&envelope.payload.path).into(),
+                    Arc::<Path>::from_proto(envelope.payload.path.clone()),
                     local_settings_kind_from_proto(kind),
                     envelope.payload.content,
                 )],
@@ -551,7 +550,7 @@ impl SettingsObserver {
                     .send(proto::UpdateWorktreeSettings {
                         project_id: self.project_id,
                         worktree_id: remote_worktree_id.to_proto(),
-                        path: directory.to_string_lossy().into_owned(),
+                        path: directory.to_proto(),
                         content: file_content,
                         kind: Some(local_settings_kind_to_proto(kind).into()),
                     })

crates/project/src/toolchain_store.rs 🔗

@@ -1,4 +1,4 @@
-use std::{str::FromStr, sync::Arc};
+use std::{path::PathBuf, str::FromStr, sync::Arc};
 
 use anyhow::{bail, Result};
 
@@ -8,7 +8,10 @@ use gpui::{
     App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task, WeakEntity,
 };
 use language::{LanguageName, LanguageRegistry, LanguageToolchainStore, Toolchain, ToolchainList};
-use rpc::{proto, AnyProtoClient, TypedEnvelope};
+use rpc::{
+    proto::{self, FromProto, ToProto},
+    AnyProtoClient, TypedEnvelope,
+};
 use settings::WorktreeId;
 use util::ResultExt as _;
 
@@ -120,7 +123,9 @@ impl ToolchainStore {
             };
             let toolchain = Toolchain {
                 name: toolchain.name.into(),
-                path: toolchain.path.into(),
+                // todo(windows)
+                // Do we need to convert path to native string?
+                path: PathBuf::from(toolchain.path).to_proto().into(),
                 as_json: serde_json::Value::from_str(&toolchain.raw_json)?,
                 language_name,
             };
@@ -144,10 +149,13 @@ impl ToolchainStore {
             .await;
 
         Ok(proto::ActiveToolchainResponse {
-            toolchain: toolchain.map(|toolchain| proto::Toolchain {
-                name: toolchain.name.into(),
-                path: toolchain.path.into(),
-                raw_json: toolchain.as_json.to_string(),
+            toolchain: toolchain.map(|toolchain| {
+                let path = PathBuf::from(toolchain.path.to_string());
+                proto::Toolchain {
+                    name: toolchain.name.into(),
+                    path: path.to_proto(),
+                    raw_json: toolchain.as_json.to_string(),
+                }
             }),
         })
     }
@@ -183,10 +191,13 @@ impl ToolchainStore {
             toolchains
                 .toolchains
                 .into_iter()
-                .map(|toolchain| proto::Toolchain {
-                    name: toolchain.name.to_string(),
-                    path: toolchain.path.to_string(),
-                    raw_json: toolchain.as_json.to_string(),
+                .map(|toolchain| {
+                    let path = PathBuf::from(toolchain.path.to_string());
+                    proto::Toolchain {
+                        name: toolchain.name.to_string(),
+                        path: path.to_proto(),
+                        raw_json: toolchain.as_json.to_string(),
+                    }
                 })
                 .collect::<Vec<_>>()
         } else {
@@ -354,6 +365,7 @@ impl RemoteToolchainStore {
         let project_id = self.project_id;
         let client = self.client.clone();
         cx.spawn(move |_| async move {
+            let path = PathBuf::from(toolchain.path.to_string());
             let _ = client
                 .request(proto::ActivateToolchain {
                     project_id,
@@ -361,7 +373,7 @@ impl RemoteToolchainStore {
                     language_name: toolchain.language_name.into(),
                     toolchain: Some(proto::Toolchain {
                         name: toolchain.name.into(),
-                        path: toolchain.path.into(),
+                        path: path.to_proto(),
                         raw_json: toolchain.as_json.to_string(),
                     }),
                 })
@@ -398,7 +410,12 @@ impl RemoteToolchainStore {
                     Some(Toolchain {
                         language_name: language_name.clone(),
                         name: toolchain.name.into(),
-                        path: toolchain.path.into(),
+                        // todo(windows)
+                        // Do we need to convert path to native string?
+                        path: PathBuf::from_proto(toolchain.path)
+                            .to_string_lossy()
+                            .to_string()
+                            .into(),
                         as_json: serde_json::Value::from_str(&toolchain.raw_json).ok()?,
                     })
                 })
@@ -439,7 +456,12 @@ impl RemoteToolchainStore {
                 Some(Toolchain {
                     language_name: language_name.clone(),
                     name: toolchain.name.into(),
-                    path: toolchain.path.into(),
+                    // todo(windows)
+                    // Do we need to convert path to native string?
+                    path: PathBuf::from_proto(toolchain.path)
+                        .to_string_lossy()
+                        .to_string()
+                        .into(),
                     as_json: serde_json::Value::from_str(&toolchain.raw_json).ok()?,
                 })
             })

crates/project/src/worktree_store.rs 🔗

@@ -15,7 +15,7 @@ use futures::{
 use gpui::{App, AsyncApp, Context, Entity, EntityId, EventEmitter, Task, WeakEntity};
 use postage::oneshot;
 use rpc::{
-    proto::{self, SSH_PROJECT_ID},
+    proto::{self, FromProto, ToProto, SSH_PROJECT_ID},
     AnyProtoClient, ErrorExt, TypedEnvelope,
 };
 use smol::{
@@ -268,10 +268,11 @@ impl WorktreeStore {
         cx.spawn(|this, mut cx| async move {
             let this = this.upgrade().context("Dropped worktree store")?;
 
+            let path = Path::new(abs_path.as_str());
             let response = client
                 .request(proto::AddWorktree {
                     project_id: SSH_PROJECT_ID,
-                    path: abs_path.clone(),
+                    path: path.to_proto(),
                     visible,
                 })
                 .await?;
@@ -282,10 +283,11 @@ impl WorktreeStore {
                 return Ok(existing_worktree);
             }
 
-            let root_name = PathBuf::from(&response.canonicalized_path)
+            let root_path_buf = PathBuf::from_proto(response.canonicalized_path.clone());
+            let root_name = root_path_buf
                 .file_name()
                 .map(|n| n.to_string_lossy().to_string())
-                .unwrap_or(response.canonicalized_path.to_string());
+                .unwrap_or(root_path_buf.to_string_lossy().to_string());
 
             let worktree = cx.update(|cx| {
                 Worktree::remote(
@@ -596,7 +598,7 @@ impl WorktreeStore {
                     id: worktree.id().to_proto(),
                     root_name: worktree.root_name().into(),
                     visible: worktree.is_visible(),
-                    abs_path: worktree.abs_path().to_string_lossy().into(),
+                    abs_path: worktree.abs_path().to_proto(),
                 }
             })
             .collect()
@@ -923,7 +925,7 @@ impl WorktreeStore {
                     project_id: remote_worktree.project_id(),
                     repository: Some(proto::ProjectPath {
                         worktree_id: project_path.worktree_id.to_proto(),
-                        path: project_path.path.to_string_lossy().to_string(), // Root path
+                        path: project_path.path.to_proto(), // Root path
                     }),
                 });
 
@@ -994,7 +996,7 @@ impl WorktreeStore {
                     project_id: remote_worktree.project_id(),
                     repository: Some(proto::ProjectPath {
                         worktree_id: repository.worktree_id.to_proto(),
-                        path: repository.path.to_string_lossy().to_string(), // Root path
+                        path: repository.path.to_proto(), // Root path
                     }),
                     branch_name: new_branch,
                 });
@@ -1116,7 +1118,7 @@ impl WorktreeStore {
             .context("Invalid GitBranches call")?;
         let project_path = ProjectPath {
             worktree_id: WorktreeId::from_proto(project_path.worktree_id),
-            path: Path::new(&project_path.path).into(),
+            path: Arc::<Path>::from_proto(project_path.path),
         };
 
         let branches = this
@@ -1147,7 +1149,7 @@ impl WorktreeStore {
             .context("Invalid GitBranches call")?;
         let project_path = ProjectPath {
             worktree_id: WorktreeId::from_proto(project_path.worktree_id),
-            path: Path::new(&project_path.path).into(),
+            path: Arc::<Path>::from_proto(project_path.path),
         };
         let new_branch = update_branch.payload.branch_name;
 

crates/proto/proto/zed.proto 🔗

@@ -2489,8 +2489,8 @@ message RefreshLlmToken {}
 // Remote FS
 
 message AddWorktree {
-    uint64 project_id = 2;
     string path = 1;
+    uint64 project_id = 2;
     bool visible = 3;
 }
 
@@ -2625,6 +2625,7 @@ message UpdateGitBranch {
     string branch_name = 2;
     ProjectPath repository = 3;
 }
+
 message GetPanicFiles {
 }
 

crates/proto/src/proto.rs 🔗

@@ -15,6 +15,8 @@ use std::{
     cmp,
     fmt::{self, Debug},
     iter, mem,
+    path::{Path, PathBuf},
+    sync::Arc,
     time::{Duration, SystemTime, UNIX_EPOCH},
 };
 
@@ -137,6 +139,62 @@ impl fmt::Display for PeerId {
     }
 }
 
+pub trait FromProto {
+    fn from_proto(proto: String) -> Self;
+}
+
+pub trait ToProto {
+    fn to_proto(self) -> String;
+}
+
+impl FromProto for PathBuf {
+    #[cfg(target_os = "windows")]
+    fn from_proto(proto: String) -> Self {
+        proto.split("/").collect()
+    }
+
+    #[cfg(not(target_os = "windows"))]
+    fn from_proto(proto: String) -> Self {
+        PathBuf::from(proto)
+    }
+}
+
+impl FromProto for Arc<Path> {
+    fn from_proto(proto: String) -> Self {
+        PathBuf::from_proto(proto).into()
+    }
+}
+
+impl ToProto for PathBuf {
+    #[cfg(target_os = "windows")]
+    fn to_proto(self) -> String {
+        self.components()
+            .map(|comp| comp.as_os_str().to_string_lossy().to_string())
+            .collect::<Vec<_>>()
+            .join("/")
+    }
+
+    #[cfg(not(target_os = "windows"))]
+    fn to_proto(self) -> String {
+        self.to_string_lossy().to_string()
+    }
+}
+
+impl ToProto for &Path {
+    #[cfg(target_os = "windows")]
+    fn to_proto(self) -> String {
+        self.components()
+            .map(|comp| comp.as_os_str().to_string_lossy().to_string())
+            .collect::<Vec<_>>()
+            .join("/")
+    }
+
+    #[cfg(not(target_os = "windows"))]
+    fn to_proto(self) -> String {
+        self.to_string_lossy().to_string()
+    }
+}
+
 messages!(
     (AcceptTermsOfService, Foreground),
     (AcceptTermsOfServiceResponse, Foreground),
@@ -757,4 +815,22 @@ mod tests {
         };
         assert_eq!(PeerId::from_u64(peer_id.as_u64()), peer_id);
     }
+
+    #[test]
+    #[cfg(target_os = "windows")]
+    fn test_proto() {
+        fn generate_proto_path(path: PathBuf) -> PathBuf {
+            let proto = path.to_proto();
+            PathBuf::from_proto(proto)
+        }
+
+        let path = PathBuf::from("C:\\foo\\bar");
+        assert_eq!(path, generate_proto_path(path.clone()));
+
+        let path = PathBuf::from("C:/foo/bar/");
+        assert_eq!(path, generate_proto_path(path.clone()));
+
+        let path = PathBuf::from("C:/foo\\bar\\");
+        assert_eq!(path, generate_proto_path(path.clone()));
+    }
 }

crates/remote_server/src/headless_project.rs 🔗

@@ -1,3 +1,4 @@
+use ::proto::{FromProto, ToProto};
 use anyhow::{anyhow, Context as _, Result};
 use extension::ExtensionHostProxy;
 use extension_host::headless_host::HeadlessExtensionStore;
@@ -325,10 +326,8 @@ impl HeadlessProject {
         mut cx: AsyncApp,
     ) -> Result<proto::AddWorktreeResponse> {
         use client::ErrorCodeExt;
-        let path = shellexpand::tilde(&message.payload.path).to_string();
-
         let fs = this.read_with(&mut cx, |this, _| this.fs.clone())?;
-        let path = PathBuf::from(path);
+        let path = PathBuf::from_proto(shellexpand::tilde(&message.payload.path).to_string());
 
         let canonicalized = match fs.canonicalize(&path).await {
             Ok(path) => path,
@@ -363,7 +362,7 @@ impl HeadlessProject {
         let response = this.update(&mut cx, |_, cx| {
             worktree.update(cx, |worktree, _| proto::AddWorktreeResponse {
                 worktree_id: worktree.id().to_proto(),
-                canonicalized_path: canonicalized.to_string_lossy().to_string(),
+                canonicalized_path: canonicalized.to_proto(),
             })
         })?;
 
@@ -418,7 +417,7 @@ impl HeadlessProject {
                 buffer_store.open_buffer(
                     ProjectPath {
                         worktree_id,
-                        path: PathBuf::from(message.payload.path).into(),
+                        path: Arc::<Path>::from_proto(message.payload.path),
                     },
                     cx,
                 )
@@ -559,11 +558,11 @@ impl HeadlessProject {
         envelope: TypedEnvelope<proto::ListRemoteDirectory>,
         cx: AsyncApp,
     ) -> Result<proto::ListRemoteDirectoryResponse> {
-        let expanded = shellexpand::tilde(&envelope.payload.path).to_string();
         let fs = cx.read_entity(&this, |this, _| this.fs.clone())?;
+        let expanded = PathBuf::from_proto(shellexpand::tilde(&envelope.payload.path).to_string());
 
         let mut entries = Vec::new();
-        let mut response = fs.read_dir(Path::new(&expanded)).await?;
+        let mut response = fs.read_dir(&expanded).await?;
         while let Some(path) = response.next().await {
             if let Some(file_name) = path?.file_name() {
                 entries.push(file_name.to_string_lossy().to_string());
@@ -578,15 +577,15 @@ impl HeadlessProject {
         cx: AsyncApp,
     ) -> Result<proto::GetPathMetadataResponse> {
         let fs = cx.read_entity(&this, |this, _| this.fs.clone())?;
-        let expanded = shellexpand::tilde(&envelope.payload.path).to_string();
+        let expanded = PathBuf::from_proto(shellexpand::tilde(&envelope.payload.path).to_string());
 
-        let metadata = fs.metadata(&PathBuf::from(expanded.clone())).await?;
+        let metadata = fs.metadata(&expanded).await?;
         let is_dir = metadata.map(|metadata| metadata.is_dir).unwrap_or(false);
 
         Ok(proto::GetPathMetadataResponse {
             exists: metadata.is_some(),
             is_dir,
-            path: expanded,
+            path: expanded.to_proto(),
         })
     }
 

crates/remote_server/src/remote_editing_tests.rs 🔗

@@ -859,7 +859,7 @@ async fn test_remote_resolve_path_in_buffer(
 async fn test_remote_resolve_abs_path(cx: &mut TestAppContext, server_cx: &mut TestAppContext) {
     let fs = FakeFs::new(server_cx.executor());
     fs.insert_tree(
-        "/code",
+        path!("/code"),
         json!({
             "project1": {
                 ".git": {},
@@ -876,7 +876,7 @@ async fn test_remote_resolve_abs_path(cx: &mut TestAppContext, server_cx: &mut T
 
     let path = project
         .update(cx, |project, cx| {
-            project.resolve_abs_path("/code/project1/README.md", cx)
+            project.resolve_abs_path(path!("/code/project1/README.md"), cx)
         })
         .await
         .unwrap();
@@ -884,12 +884,12 @@ async fn test_remote_resolve_abs_path(cx: &mut TestAppContext, server_cx: &mut T
     assert!(path.is_file());
     assert_eq!(
         path.abs_path().unwrap().to_string_lossy(),
-        "/code/project1/README.md"
+        path!("/code/project1/README.md")
     );
 
     let path = project
         .update(cx, |project, cx| {
-            project.resolve_abs_path("/code/project1/src", cx)
+            project.resolve_abs_path(path!("/code/project1/src"), cx)
         })
         .await
         .unwrap();
@@ -897,12 +897,12 @@ async fn test_remote_resolve_abs_path(cx: &mut TestAppContext, server_cx: &mut T
     assert!(path.is_dir());
     assert_eq!(
         path.abs_path().unwrap().to_string_lossy(),
-        "/code/project1/src"
+        path!("/code/project1/src")
     );
 
     let path = project
         .update(cx, |project, cx| {
-            project.resolve_abs_path("/code/project1/DOESNOTEXIST", cx)
+            project.resolve_abs_path(path!("/code/project1/DOESNOTEXIST"), cx)
         })
         .await;
     assert!(path.is_none());
@@ -958,7 +958,7 @@ async fn test_adding_then_removing_then_adding_worktrees(
 ) {
     let fs = FakeFs::new(server_cx.executor());
     fs.insert_tree(
-        "/code",
+        path!("/code"),
         json!({
             "project1": {
                 ".git": {},
@@ -977,14 +977,14 @@ async fn test_adding_then_removing_then_adding_worktrees(
     let (project, _headless) = init_test(&fs, cx, server_cx).await;
     let (_worktree, _) = project
         .update(cx, |project, cx| {
-            project.find_or_create_worktree("/code/project1", true, cx)
+            project.find_or_create_worktree(path!("/code/project1"), true, cx)
         })
         .await
         .unwrap();
 
     let (worktree_2, _) = project
         .update(cx, |project, cx| {
-            project.find_or_create_worktree("/code/project2", true, cx)
+            project.find_or_create_worktree(path!("/code/project2"), true, cx)
         })
         .await
         .unwrap();
@@ -994,7 +994,7 @@ async fn test_adding_then_removing_then_adding_worktrees(
 
     let (worktree_2, _) = project
         .update(cx, |project, cx| {
-            project.find_or_create_worktree("/code/project2", true, cx)
+            project.find_or_create_worktree(path!("/code/project2"), true, cx)
         })
         .await
         .unwrap();

crates/worktree/src/worktree.rs 🔗

@@ -39,7 +39,7 @@ use postage::{
     watch,
 };
 use rpc::{
-    proto::{self, split_worktree_update},
+    proto::{self, split_worktree_update, FromProto, ToProto},
     AnyProtoClient,
 };
 pub use settings::WorktreeId;
@@ -283,13 +283,13 @@ impl RepositoryEntry {
                             current_new_entry = new_statuses.next();
                         }
                         Ordering::Greater => {
-                            removed_statuses.push(old_entry.repo_path.to_proto());
+                            removed_statuses.push(old_entry.repo_path.as_ref().to_proto());
                             current_old_entry = old_statuses.next();
                         }
                     }
                 }
                 (None, Some(old_entry)) => {
-                    removed_statuses.push(old_entry.repo_path.to_proto());
+                    removed_statuses.push(old_entry.repo_path.as_ref().to_proto());
                     current_old_entry = old_statuses.next();
                 }
                 (Some(new_entry), None) => {
@@ -308,7 +308,7 @@ impl RepositoryEntry {
             current_merge_conflicts: self
                 .current_merge_conflicts
                 .iter()
-                .map(RepoPath::to_proto)
+                .map(|path| path.as_ref().to_proto())
                 .collect(),
         }
     }
@@ -700,7 +700,7 @@ impl Worktree {
             let snapshot = Snapshot::new(
                 worktree.id,
                 worktree.root_name,
-                Arc::from(PathBuf::from(worktree.abs_path)),
+                Arc::<Path>::from_proto(worktree.abs_path),
             );
 
             let background_snapshot = Arc::new(Mutex::new((snapshot.clone(), Vec::new())));
@@ -849,7 +849,7 @@ impl Worktree {
             id: self.id().to_proto(),
             root_name: self.root_name().to_string(),
             visible: self.is_visible(),
-            abs_path: self.abs_path().as_os_str().to_string_lossy().into(),
+            abs_path: self.abs_path().to_proto(),
         }
     }
 
@@ -1007,7 +1007,7 @@ impl Worktree {
         is_directory: bool,
         cx: &Context<Worktree>,
     ) -> Task<Result<CreatedEntry>> {
-        let path = path.into();
+        let path: Arc<Path> = path.into();
         let worktree_id = self.id();
         match self {
             Worktree::Local(this) => this.create_entry(path, is_directory, cx),
@@ -1016,7 +1016,7 @@ impl Worktree {
                 let request = this.client.request(proto::CreateProjectEntry {
                     worktree_id: worktree_id.to_proto(),
                     project_id,
-                    path: path.to_string_lossy().into(),
+                    path: path.as_ref().to_proto(),
                     is_directory,
                 });
                 cx.spawn(move |this, mut cx| async move {
@@ -1101,21 +1101,19 @@ impl Worktree {
         new_path: impl Into<Arc<Path>>,
         cx: &Context<Self>,
     ) -> Task<Result<Option<Entry>>> {
-        let new_path = new_path.into();
+        let new_path: Arc<Path> = new_path.into();
         match self {
             Worktree::Local(this) => {
                 this.copy_entry(entry_id, relative_worktree_source_path, new_path, cx)
             }
             Worktree::Remote(this) => {
-                let relative_worktree_source_path =
-                    relative_worktree_source_path.map(|relative_worktree_source_path| {
-                        relative_worktree_source_path.to_string_lossy().into()
-                    });
+                let relative_worktree_source_path = relative_worktree_source_path
+                    .map(|relative_worktree_source_path| relative_worktree_source_path.to_proto());
                 let response = this.client.request(proto::CopyProjectEntry {
                     project_id: this.project_id,
                     entry_id: entry_id.to_proto(),
                     relative_worktree_source_path,
-                    new_path: new_path.to_string_lossy().into(),
+                    new_path: new_path.to_proto(),
                 });
                 cx.spawn(move |this, mut cx| async move {
                     let response = response.await?;
@@ -1214,7 +1212,11 @@ impl Worktree {
         let (scan_id, entry) = this.update(&mut cx, |this, cx| {
             (
                 this.scan_id(),
-                this.create_entry(PathBuf::from(request.path), request.is_directory, cx),
+                this.create_entry(
+                    Arc::<Path>::from_proto(request.path),
+                    request.is_directory,
+                    cx,
+                ),
             )
         })?;
         Ok(proto::ProjectEntryResponse {
@@ -1288,7 +1290,7 @@ impl Worktree {
                 this.scan_id(),
                 this.rename_entry(
                     ProjectEntryId::from_proto(request.entry_id),
-                    PathBuf::from(request.new_path),
+                    Arc::<Path>::from_proto(request.new_path),
                     cx,
                 ),
             )
@@ -1308,14 +1310,15 @@ impl Worktree {
         mut cx: AsyncApp,
     ) -> Result<proto::ProjectEntryResponse> {
         let (scan_id, task) = this.update(&mut cx, |this, cx| {
-            let relative_worktree_source_path =
-                request.relative_worktree_source_path.map(PathBuf::from);
+            let relative_worktree_source_path = request
+                .relative_worktree_source_path
+                .map(PathBuf::from_proto);
             (
                 this.scan_id(),
                 this.copy_entry(
                     ProjectEntryId::from_proto(request.entry_id),
                     relative_worktree_source_path,
-                    PathBuf::from(request.new_path),
+                    PathBuf::from_proto(request.new_path),
                     cx,
                 ),
             )
@@ -2368,11 +2371,11 @@ impl RemoteWorktree {
         new_path: impl Into<Arc<Path>>,
         cx: &Context<Worktree>,
     ) -> Task<Result<CreatedEntry>> {
-        let new_path = new_path.into();
+        let new_path: Arc<Path> = new_path.into();
         let response = self.client.request(proto::RenameProjectEntry {
             project_id: self.project_id,
             entry_id: entry_id.to_proto(),
-            new_path: new_path.to_string_lossy().into(),
+            new_path: new_path.as_ref().to_proto(),
         });
         cx.spawn(move |this, mut cx| async move {
             let response = response.await?;
@@ -2454,7 +2457,7 @@ impl Snapshot {
         proto::UpdateWorktree {
             project_id,
             worktree_id,
-            abs_path: self.abs_path().to_string_lossy().into(),
+            abs_path: self.abs_path().to_proto(),
             root_name: self.root_name().to_string(),
             updated_entries,
             removed_entries: Vec::new(),
@@ -2555,7 +2558,7 @@ impl Snapshot {
             update.removed_entries.len()
         );
         self.update_abs_path(
-            SanitizedPath::from(PathBuf::from(update.abs_path)),
+            SanitizedPath::from(PathBuf::from_proto(update.abs_path)),
             update.root_name,
         );
 
@@ -2617,7 +2620,7 @@ impl Snapshot {
                     let edits = repository
                         .removed_statuses
                         .into_iter()
-                        .map(|path| Edit::Remove(PathKey(Path::new(&path).into())))
+                        .map(|path| Edit::Remove(PathKey(FromProto::from_proto(path))))
                         .chain(repository.updated_statuses.into_iter().filter_map(
                             |updated_status| {
                                 Some(Edit::Insert(updated_status.try_into().log_err()?))
@@ -2952,7 +2955,7 @@ impl LocalSnapshot {
         proto::UpdateWorktree {
             project_id,
             worktree_id,
-            abs_path: self.abs_path().to_string_lossy().into(),
+            abs_path: self.abs_path().to_proto(),
             root_name: self.root_name().to_string(),
             updated_entries,
             removed_entries,
@@ -3635,7 +3638,7 @@ impl language::File for File {
         rpc::proto::File {
             worktree_id: self.worktree.read(cx).id().to_proto(),
             entry_id: self.entry_id.map(|id| id.to_proto()),
-            path: self.path.to_string_lossy().into(),
+            path: self.path.as_ref().to_proto(),
             mtime: self.disk_state.mtime().map(|time| time.into()),
             is_deleted: self.disk_state == DiskState::Deleted,
         }
@@ -3716,7 +3719,7 @@ impl File {
 
         Ok(Self {
             worktree,
-            path: Path::new(&proto.path).into(),
+            path: Arc::<Path>::from_proto(proto.path),
             disk_state,
             entry_id: proto.entry_id.map(ProjectEntryId::from_proto),
             is_local: false,
@@ -3835,8 +3838,9 @@ impl StatusEntry {
                 index_status
             }),
         };
+
         proto::StatusEntry {
-            repo_path: self.repo_path.to_proto(),
+            repo_path: self.repo_path.as_ref().to_proto(),
             simple_status,
             status: Some(status_to_proto(self.status)),
         }
@@ -3847,7 +3851,7 @@ impl TryFrom<proto::StatusEntry> for StatusEntry {
     type Error = anyhow::Error;
 
     fn try_from(value: proto::StatusEntry) -> Result<Self, Self::Error> {
-        let repo_path = RepoPath(Path::new(&value.repo_path).into());
+        let repo_path = RepoPath(Arc::<Path>::from_proto(value.repo_path));
         let status = status_from_proto(value.simple_status, value.status)?;
         Ok(Self { repo_path, status })
     }
@@ -6231,7 +6235,7 @@ impl<'a> From<&'a Entry> for proto::Entry {
         Self {
             id: entry.id.to_proto(),
             is_dir: entry.is_dir(),
-            path: entry.path.to_string_lossy().into(),
+            path: entry.path.as_ref().to_proto(),
             inode: entry.inode,
             mtime: entry.mtime.map(|time| time.into()),
             is_ignored: entry.is_ignored,
@@ -6241,7 +6245,7 @@ impl<'a> From<&'a Entry> for proto::Entry {
             canonical_path: entry
                 .canonical_path
                 .as_ref()
-                .map(|path| path.to_string_lossy().to_string()),
+                .map(|path| path.as_ref().to_proto()),
         }
     }
 }
@@ -6257,20 +6261,22 @@ impl<'a> TryFrom<(&'a CharBag, &PathMatcher, proto::Entry)> for Entry {
         } else {
             EntryKind::File
         };
-        let path: Arc<Path> = PathBuf::from(entry.path).into();
+
+        let path = Arc::<Path>::from_proto(entry.path);
         let char_bag = char_bag_for_path(*root_char_bag, &path);
+        let is_always_included = always_included.is_match(path.as_ref());
         Ok(Entry {
             id: ProjectEntryId::from_proto(entry.id),
             kind,
-            path: path.clone(),
+            path,
             inode: entry.inode,
             mtime: entry.mtime.map(|time| time.into()),
             size: entry.size.unwrap_or(0),
             canonical_path: entry
                 .canonical_path
-                .map(|path_string| Box::from(Path::new(&path_string))),
+                .map(|path_string| Box::from(PathBuf::from_proto(path_string))),
             is_ignored: entry.is_ignored,
-            is_always_included: always_included.is_match(path.as_ref()),
+            is_always_included,
             is_external: entry.is_external,
             is_private: false,
             char_bag,