remote: Support opening builtin host files in remote workspaces on wsl (#46910)

Lukas Wirth created

Release Notes:

- Opening bundled files, keymap, and local release notes now opens in
remote windows instead of opening a new local zed window
- Opening the settings files, keymap files, task files, debug files and
logs will now open within wsl windows instead of opening a new local zed
window

Change summary

crates/activity_indicator/src/activity_indicator.rs |   2 
crates/agent/src/outline.rs                         |   2 
crates/agent_ui/src/acp/thread_view.rs              |   5 
crates/auto_update_ui/src/auto_update_ui.rs         | 135 ++++---
crates/collab/src/tests/integration_tests.rs        |   2 
crates/edit_prediction/src/udiff.rs                 |   4 
crates/edit_prediction_ui/src/edit_prediction_ui.rs |   4 
crates/editor/src/editor.rs                         |   4 
crates/editor/src/items.rs                          |   7 
crates/editor/src/rust_analyzer_ext.rs              |   5 
crates/fs/src/fs.rs                                 |  26 +
crates/keymap_editor/src/keymap_editor.rs           |  63 +--
crates/language_tools/src/lsp_button.rs             |   2 
crates/project/src/buffer_store.rs                  |  34 +
crates/project/src/git_store.rs                     |  17 
crates/project/src/project.rs                       |  47 ++
crates/project/src/project_tests.rs                 |   2 
crates/project/src/worktree_store.rs                |   4 
crates/remote/src/remote_client.rs                  |   5 
crates/remote/src/transport/wsl.rs                  |  86 +++--
crates/remote_server/src/headless_project.rs        |   6 
crates/settings_ui/src/settings_ui.rs               |  57 ++-
crates/util/src/paths.rs                            |  13 
crates/workspace/src/workspace.rs                   |  78 +++-
crates/zed/src/main.rs                              |   7 
crates/zed/src/zed.rs                               | 255 ++++++++------
26 files changed, 524 insertions(+), 348 deletions(-)

Detailed changes

crates/activity_indicator/src/activity_indicator.rs 🔗

@@ -234,7 +234,7 @@ impl ActivityIndicator {
                 status,
             } => {
                 let create_buffer =
-                    project.update(cx, |project, cx| project.create_buffer(false, cx));
+                    project.update(cx, |project, cx| project.create_buffer(None, false, cx));
                 let status = status.clone();
                 let server_name = server_name.clone();
                 cx.spawn_in(window, async move |workspace, cx| {

crates/agent/src/outline.rs 🔗

@@ -179,7 +179,7 @@ mod tests {
         let content = "⚡".repeat(100 * 1024); // 100KB
         let content_len = content.len();
         let buffer = project
-            .update(cx, |project, cx| project.create_buffer(true, cx))
+            .update(cx, |project, cx| project.create_buffer(None, true, cx))
             .await
             .expect("failed to create buffer");
 

crates/agent_ui/src/acp/thread_view.rs 🔗

@@ -6742,12 +6742,13 @@ impl AcpThreadView {
             let markdown_language = markdown_language_task.await?;
 
             let buffer = project
-                .update(cx, |project, cx| project.create_buffer(false, cx))
+                .update(cx, |project, cx| {
+                    project.create_buffer(Some(markdown_language), false, cx)
+                })
                 .await?;
 
             buffer.update(cx, |buffer, cx| {
                 buffer.set_text(markdown, cx);
-                buffer.set_language(Some(markdown_language), cx);
                 buffer.set_capability(language::Capability::ReadWrite, cx);
             });
 

crates/auto_update_ui/src/auto_update_ui.rs 🔗

@@ -5,7 +5,7 @@ use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPrevi
 use release_channel::{AppVersion, ReleaseChannel};
 use serde::Deserialize;
 use smol::io::AsyncReadExt;
-use util::ResultExt as _;
+use util::{ResultExt as _, maybe};
 use workspace::Workspace;
 use workspace::notifications::ErrorMessagePrompt;
 use workspace::notifications::simple_message_notification::MessageNotification;
@@ -39,14 +39,14 @@ struct ReleaseNotesBody {
     release_notes: String,
 }
 
-fn notify_release_notes_failed_to_show_locally(
+fn notify_release_notes_failed_to_show(
     workspace: &mut Workspace,
     _window: &mut Window,
     cx: &mut Context<Workspace>,
 ) {
-    struct ViewReleaseNotesLocallyError;
+    struct ViewReleaseNotesError;
     workspace.show_notification(
-        NotificationId::unique::<ViewReleaseNotesLocallyError>(),
+        NotificationId::unique::<ViewReleaseNotesError>(),
         cx,
         |cx| {
             cx.new(move |cx| {
@@ -92,70 +92,71 @@ fn view_release_notes_locally(
         .languages
         .language_for_name("Markdown");
 
-    workspace
-        .with_local_workspace(window, cx, move |_, window, cx| {
-            cx.spawn_in(window, async move |workspace, cx| {
-                let markdown = markdown.await.log_err();
-                let response = client.get(&url, Default::default(), true).await;
-                let Some(mut response) = response.log_err() else {
-                    workspace
-                        .update_in(cx, notify_release_notes_failed_to_show_locally)
-                        .log_err();
-                    return;
-                };
-
-                let mut body = Vec::new();
-                response.body_mut().read_to_end(&mut body).await.ok();
-
-                let body: serde_json::Result<ReleaseNotesBody> =
-                    serde_json::from_slice(body.as_slice());
-
-                if let Ok(body) = body {
-                    workspace
-                        .update_in(cx, |workspace, window, cx| {
-                            let project = workspace.project().clone();
-                            let buffer = project.update(cx, |project, cx| {
-                                project.create_local_buffer("", markdown, false, cx)
-                            });
-                            buffer.update(cx, |buffer, cx| {
-                                buffer.edit([(0..0, body.release_notes)], None, cx)
-                            });
-                            let language_registry = project.read(cx).languages().clone();
-
-                            let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
-
-                            let editor = cx.new(|cx| {
-                                Editor::for_multibuffer(buffer, Some(project), window, cx)
-                            });
-                            let workspace_handle = workspace.weak_handle();
-                            let markdown_preview: Entity<MarkdownPreviewView> =
-                                MarkdownPreviewView::new(
-                                    MarkdownPreviewMode::Default,
-                                    editor,
-                                    workspace_handle,
-                                    language_registry,
-                                    window,
-                                    cx,
-                                );
-                            workspace.add_item_to_active_pane(
-                                Box::new(markdown_preview),
-                                None,
-                                true,
-                                window,
-                                cx,
-                            );
-                            cx.notify();
-                        })
-                        .log_err();
-                } else {
-                    workspace
-                        .update_in(cx, notify_release_notes_failed_to_show_locally)
-                        .log_err();
-                }
-            })
-            .detach();
+    cx.spawn_in(window, async move |workspace, cx| {
+        let markdown = markdown.await.log_err();
+        let response = client.get(&url, Default::default(), true).await;
+        let Some(mut response) = response.log_err() else {
+            workspace
+                .update_in(cx, notify_release_notes_failed_to_show)
+                .log_err();
+            return;
+        };
+
+        let mut body = Vec::new();
+        response.body_mut().read_to_end(&mut body).await.ok();
+
+        let body: serde_json::Result<ReleaseNotesBody> = serde_json::from_slice(body.as_slice());
+
+        let res: Option<()> = maybe!(async {
+            let body = body.ok()?;
+            let project = workspace
+                .read_with(cx, |workspace, _| workspace.project().clone())
+                .ok()?;
+            let (language_registry, buffer) = project.update(cx, |project, cx| {
+                (
+                    project.languages().clone(),
+                    project.create_buffer(markdown, false, cx),
+                )
+            });
+            let buffer = buffer.await.ok()?;
+            buffer.update(cx, |buffer, cx| {
+                buffer.edit([(0..0, body.release_notes)], None, cx)
+            });
+
+            let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
+
+            let ws_handle = workspace.clone();
+            workspace
+                .update_in(cx, |workspace, window, cx| {
+                    let editor =
+                        cx.new(|cx| Editor::for_multibuffer(buffer, Some(project), window, cx));
+                    let markdown_preview: Entity<MarkdownPreviewView> = MarkdownPreviewView::new(
+                        MarkdownPreviewMode::Default,
+                        editor,
+                        ws_handle,
+                        language_registry,
+                        window,
+                        cx,
+                    );
+                    workspace.add_item_to_active_pane(
+                        Box::new(markdown_preview),
+                        None,
+                        true,
+                        window,
+                        cx,
+                    );
+                    cx.notify();
+                })
+                .ok()
         })
-        .detach();
+        .await;
+        if res.is_none() {
+            workspace
+                .update_in(cx, notify_release_notes_failed_to_show)
+                .log_err();
+        }
+    })
+    .detach();
 }
 
 /// Shows a notification across all workspaces if an update was previously automatically installed

crates/collab/src/tests/integration_tests.rs 🔗

@@ -2502,7 +2502,7 @@ async fn test_propagate_saves_and_fs_changes(
     });
 
     let new_buffer_a = project_a
-        .update(cx_a, |p, cx| p.create_buffer(false, cx))
+        .update(cx_a, |p, cx| p.create_buffer(None, false, cx))
         .await
         .unwrap();
 

crates/edit_prediction/src/udiff.rs 🔗

@@ -81,7 +81,9 @@ pub async fn apply_diff(
                             Entry::Vacant(entry) => {
                                 let buffer: Entity<Buffer> = if status == FileStatus::Created {
                                     project
-                                        .update(cx, |project, cx| project.create_buffer(true, cx))
+                                        .update(cx, |project, cx| {
+                                            project.create_buffer(None, true, cx)
+                                        })
                                         .await?
                                 } else {
                                     let project_path = project

crates/edit_prediction_ui/src/edit_prediction_ui.rs 🔗

@@ -173,7 +173,9 @@ fn capture_example_as_markdown(
                 .await?
         } else {
             project
-                .update(cx, |project, cx| project.create_buffer(false, cx))
+                .update(cx, |project, cx| {
+                    project.create_buffer(Some(markdown_language.clone()), false, cx)
+                })
                 .await?
         };
 

crates/editor/src/editor.rs 🔗

@@ -2929,7 +2929,7 @@ impl Editor {
         cx: &mut Context<Workspace>,
     ) -> Task<Result<Entity<Editor>>> {
         let project = workspace.project().clone();
-        let create = project.update(cx, |project, cx| project.create_buffer(true, cx));
+        let create = project.update(cx, |project, cx| project.create_buffer(None, true, cx));
 
         cx.spawn_in(window, async move |workspace, cx| {
             let buffer = create.await?;
@@ -2976,7 +2976,7 @@ impl Editor {
         cx: &mut Context<Workspace>,
     ) {
         let project = workspace.project().clone();
-        let create = project.update(cx, |project, cx| project.create_buffer(true, cx));
+        let create = project.update(cx, |project, cx| project.create_buffer(None, true, cx));
 
         cx.spawn_in(window, async move |workspace, cx| {
             let buffer = create.await?;

crates/editor/src/items.rs 🔗

@@ -1116,16 +1116,13 @@ impl SerializableItem for Editor {
 
                     // First create the empty buffer
                     let buffer = project
-                        .update(cx, |project, cx| project.create_buffer(true, cx))
+                        .update(cx, |project, cx| project.create_buffer(language, true, cx))
                         .await
                         .context("Failed to create buffer while deserializing editor")?;
 
                     // Then set the text so that the dirty bit is set correctly
                     buffer.update(cx, |buffer, cx| {
                         buffer.set_language_registry(language_registry);
-                        if let Some(language) = language {
-                            buffer.set_language(Some(language), cx);
-                        }
                         buffer.set_text(contents, cx);
                         if let Some(entry) = buffer.peek_undo_stack() {
                             buffer.forget_transaction(entry.transaction_id());
@@ -1227,7 +1224,7 @@ impl SerializableItem for Editor {
                 ..
             } => window.spawn(cx, async move |cx| {
                 let buffer = project
-                    .update(cx, |project, cx| project.create_buffer(true, cx))
+                    .update(cx, |project, cx| project.create_buffer(None, true, cx))
                     .await
                     .context("Failed to create buffer")?;
 

crates/editor/src/rust_analyzer_ext.rs 🔗

@@ -200,12 +200,13 @@ pub fn expand_macro_recursively(
         }
 
         let buffer = project
-            .update(cx, |project, cx| project.create_buffer(false, cx))
+            .update(cx, |project, cx| {
+                project.create_buffer(Some(rust_language), false, cx)
+            })
             .await?;
         workspace.update_in(cx, |workspace, window, cx| {
             buffer.update(cx, |buffer, cx| {
                 buffer.set_text(macro_expansion.expansion, cx);
-                buffer.set_language(Some(rust_language), cx);
                 buffer.set_capability(Capability::ReadOnly, cx);
             });
             let multibuffer =

crates/fs/src/fs.rs 🔗

@@ -545,7 +545,10 @@ impl Fs for RealFs {
         } else if !options.ignore_if_exists {
             open_options.create_new(true);
         }
-        open_options.open(path).await?;
+        open_options
+            .open(path)
+            .await
+            .with_context(|| format!("Failed to create file at {:?}", path))?;
         Ok(())
     }
 
@@ -554,7 +557,9 @@ impl Fs for RealFs {
         path: &Path,
         content: Pin<&mut (dyn AsyncRead + Send)>,
     ) -> Result<()> {
-        let mut file = smol::fs::File::create(&path).await?;
+        let mut file = smol::fs::File::create(&path)
+            .await
+            .with_context(|| format!("Failed to create file at {:?}", path))?;
         futures::io::copy(content, &mut file).await?;
         Ok(())
     }
@@ -748,7 +753,10 @@ impl Fs for RealFs {
     async fn load(&self, path: &Path) -> Result<String> {
         let path = path.to_path_buf();
         self.executor
-            .spawn(async move { Ok(std::fs::read_to_string(path)?) })
+            .spawn(async move {
+                std::fs::read_to_string(&path)
+                    .with_context(|| format!("Failed to read file {}", path.display()))
+            })
             .await
     }
 
@@ -810,9 +818,13 @@ impl Fs for RealFs {
     async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()> {
         let buffer_size = text.summary().len.min(10 * 1024);
         if let Some(path) = path.parent() {
-            self.create_dir(path).await?;
+            self.create_dir(path)
+                .await
+                .with_context(|| format!("Failed to create directory at {:?}", path))?;
         }
-        let file = smol::fs::File::create(path).await?;
+        let file = smol::fs::File::create(path)
+            .await
+            .with_context(|| format!("Failed to create file at {:?}", path))?;
         let mut writer = smol::io::BufWriter::with_capacity(buffer_size, file);
         for chunk in text::chunks_with_line_ending(text, line_ending) {
             writer.write_all(chunk.as_bytes()).await?;
@@ -823,7 +835,9 @@ impl Fs for RealFs {
 
     async fn write(&self, path: &Path, content: &[u8]) -> Result<()> {
         if let Some(path) = path.parent() {
-            self.create_dir(path).await?;
+            self.create_dir(path)
+                .await
+                .with_context(|| format!("Failed to create directory at {:?}", path))?;
         }
         let path = path.to_owned();
         let contents = content.to_owned();

crates/keymap_editor/src/keymap_editor.rs 🔗

@@ -92,43 +92,38 @@ pub fn init(cx: &mut App) {
         window: &mut Window,
         cx: &mut Context<Workspace>,
     ) {
-        workspace
-            .with_local_workspace(window, cx, |workspace, window, cx| {
-                let existing = workspace
-                    .active_pane()
-                    .read(cx)
-                    .items()
-                    .find_map(|item| item.downcast::<KeymapEditor>());
-
-                let keymap_editor = if let Some(existing) = existing {
-                    workspace.activate_item(&existing, true, true, window, cx);
-                    existing
-                } else {
-                    let keymap_editor =
-                        cx.new(|cx| KeymapEditor::new(workspace.weak_handle(), window, cx));
-                    workspace.add_item_to_active_pane(
-                        Box::new(keymap_editor.clone()),
-                        None,
-                        true,
-                        window,
-                        cx,
-                    );
-                    keymap_editor
-                };
+        let existing = workspace
+            .active_pane()
+            .read(cx)
+            .items()
+            .find_map(|item| item.downcast::<KeymapEditor>());
 
-                if let Some(filter) = filter {
-                    keymap_editor.update(cx, |editor, cx| {
-                        editor.filter_editor.update(cx, |editor, cx| {
-                            editor.clear(window, cx);
-                            editor.insert(&filter, window, cx);
-                        });
-                        if !editor.has_binding_for(&filter) {
-                            open_binding_modal_after_loading(cx)
-                        }
-                    })
+        let keymap_editor = if let Some(existing) = existing {
+            workspace.activate_item(&existing, true, true, window, cx);
+            existing
+        } else {
+            let keymap_editor = cx.new(|cx| KeymapEditor::new(workspace.weak_handle(), window, cx));
+            workspace.add_item_to_active_pane(
+                Box::new(keymap_editor.clone()),
+                None,
+                true,
+                window,
+                cx,
+            );
+            keymap_editor
+        };
+
+        if let Some(filter) = filter {
+            keymap_editor.update(cx, |editor, cx| {
+                editor.filter_editor.update(cx, |editor, cx| {
+                    editor.clear(window, cx);
+                    editor.insert(&filter, window, cx);
+                });
+                if !editor.has_binding_for(&filter) {
+                    open_binding_modal_after_loading(cx)
                 }
             })
-            .detach_and_log_err(cx);
+        }
     }
 
     cx.observe_new(|workspace: &mut Workspace, _window, _cx| {

crates/language_tools/src/lsp_button.rs 🔗

@@ -316,7 +316,7 @@ impl LanguageServerState {
                                 let Some(create_buffer) = workspace_for_message
                                     .update(cx, |workspace, cx| {
                                         workspace.project().update(cx, |project, cx| {
-                                            project.create_buffer(false, cx)
+                                            project.create_buffer(None, false, cx)
                                         })
                                     })
                                     .ok()

crates/project/src/buffer_store.rs 🔗

@@ -331,6 +331,7 @@ impl RemoteBufferStore {
 
     fn create_buffer(
         &self,
+        language: Option<Arc<Language>>,
         project_searchable: bool,
         cx: &mut Context<BufferStore>,
     ) -> Task<Result<Entity<Buffer>>> {
@@ -341,13 +342,20 @@ impl RemoteBufferStore {
             let response = create.await?;
             let buffer_id = BufferId::new(response.buffer_id)?;
 
-            this.update(cx, |this, cx| {
-                if !project_searchable {
-                    this.non_searchable_buffers.insert(buffer_id);
-                }
-                this.wait_for_remote_buffer(buffer_id, cx)
-            })?
-            .await
+            let buffer = this
+                .update(cx, |this, cx| {
+                    if !project_searchable {
+                        this.non_searchable_buffers.insert(buffer_id);
+                    }
+                    this.wait_for_remote_buffer(buffer_id, cx)
+                })?
+                .await?;
+            if let Some(language) = language {
+                buffer.update(cx, |buffer, cx| {
+                    buffer.set_language(Some(language), cx);
+                });
+            }
+            Ok(buffer)
         })
     }
 
@@ -710,12 +718,15 @@ impl LocalBufferStore {
 
     fn create_buffer(
         &self,
+        language: Option<Arc<Language>>,
         project_searchable: bool,
         cx: &mut Context<BufferStore>,
     ) -> Task<Result<Entity<Buffer>>> {
         cx.spawn(async move |buffer_store, cx| {
-            let buffer =
-                cx.new(|cx| Buffer::local("", cx).with_language(language::PLAIN_TEXT.clone(), cx));
+            let buffer = cx.new(|cx| {
+                Buffer::local("", cx)
+                    .with_language(language.unwrap_or_else(|| language::PLAIN_TEXT.clone()), cx)
+            });
             buffer_store.update(cx, |buffer_store, cx| {
                 buffer_store.add_buffer(buffer.clone(), cx).log_err();
                 if !project_searchable {
@@ -900,12 +911,13 @@ impl BufferStore {
 
     pub fn create_buffer(
         &mut self,
+        language: Option<Arc<Language>>,
         project_searchable: bool,
         cx: &mut Context<Self>,
     ) -> Task<Result<Entity<Buffer>>> {
         match &self.state {
-            BufferStoreState::Local(this) => this.create_buffer(project_searchable, cx),
-            BufferStoreState::Remote(this) => this.create_buffer(project_searchable, cx),
+            BufferStoreState::Local(this) => this.create_buffer(language, project_searchable, cx),
+            BufferStoreState::Remote(this) => this.create_buffer(language, project_searchable, cx),
         }
     }
 

crates/project/src/git_store.rs 🔗

@@ -3966,17 +3966,18 @@ impl Repository {
         cx: &mut Context<Self>,
     ) -> Task<Result<Entity<Buffer>>> {
         cx.spawn(async move |repository, cx| {
+            let git_commit_language = match language_registry {
+                Some(language_registry) => {
+                    Some(language_registry.language_for_name("Git Commit").await?)
+                }
+                None => None,
+            };
             let buffer = buffer_store
-                .update(cx, |buffer_store, cx| buffer_store.create_buffer(false, cx))
+                .update(cx, |buffer_store, cx| {
+                    buffer_store.create_buffer(git_commit_language, false, cx)
+                })
                 .await?;
 
-            if let Some(language_registry) = language_registry {
-                let git_commit_language = language_registry.language_for_name("Git Commit").await?;
-                buffer.update(cx, |buffer, cx| {
-                    buffer.set_language(Some(git_commit_language), cx);
-                });
-            }
-
             repository.update(cx, |repository, _| {
                 repository.commit_message_buffer = Some(buffer.clone());
             })?;

crates/project/src/project.rs 🔗

@@ -35,6 +35,7 @@ pub mod search_history;
 mod yarn;
 
 use dap::inline_value::{InlineValueLocation, VariableLookupKind, VariableScope};
+use itertools::Either;
 
 use crate::{
     git_store::GitStore,
@@ -198,6 +199,7 @@ pub struct Project {
     user_store: Entity<UserStore>,
     fs: Arc<dyn Fs>,
     remote_client: Option<Entity<RemoteClient>>,
+    // todo lw explain the client_state x remote_client matrix, its super confusing
     client_state: ProjectClientState,
     git_store: Entity<GitStore>,
     collaborators: HashMap<proto::PeerId, Collaborator>,
@@ -2727,6 +2729,19 @@ impl Project {
         !self.is_local()
     }
 
+    #[inline]
+    pub fn is_via_wsl_with_host_interop(&self, cx: &App) -> bool {
+        match &self.client_state {
+            ProjectClientState::Local | ProjectClientState::Shared { .. } => {
+                matches!(
+                    &self.remote_client, Some(remote_client)
+                    if remote_client.read(cx).has_wsl_interop()
+                )
+            }
+            _ => false,
+        }
+    }
+
     pub fn disable_worktree_scanner(&mut self, cx: &mut Context<Self>) {
         self.worktree_store.update(cx, |worktree_store, _cx| {
             worktree_store.disable_scanner();
@@ -2736,11 +2751,12 @@ impl Project {
     #[inline]
     pub fn create_buffer(
         &mut self,
-        searchable: bool,
+        language: Option<Arc<Language>>,
+        project_searchable: bool,
         cx: &mut Context<Self>,
     ) -> Task<Result<Entity<Buffer>>> {
         self.buffer_store.update(cx, |buffer_store, cx| {
-            buffer_store.create_buffer(searchable, cx)
+            buffer_store.create_buffer(language, project_searchable, cx)
         })
     }
 
@@ -4240,6 +4256,31 @@ impl Project {
         })
     }
 
+    /// Attempts to convert the input path to a WSL path if this is a wsl remote project and the input path is a host windows path.
+    pub fn try_windows_path_to_wsl(
+        &self,
+        abs_path: &Path,
+        cx: &App,
+    ) -> impl Future<Output = Result<PathBuf>> + use<> {
+        let fut = if cfg!(windows)
+            && let (
+                ProjectClientState::Local | ProjectClientState::Shared { .. },
+                Some(remote_client),
+            ) = (&self.client_state, &self.remote_client)
+            && let RemoteConnectionOptions::Wsl(wsl) = remote_client.read(cx).connection_options()
+        {
+            Either::Left(wsl.abs_windows_path_to_wsl_path(abs_path))
+        } else {
+            Either::Right(abs_path.to_owned())
+        };
+        async move {
+            match fut {
+                Either::Left(fut) => fut.await.map(Into::into),
+                Either::Right(path) => Ok(path),
+            }
+        }
+    }
+
     pub fn find_or_create_worktree(
         &mut self,
         abs_path: impl AsRef<Path>,
@@ -5183,7 +5224,7 @@ impl Project {
         mut cx: AsyncApp,
     ) -> Result<proto::OpenBufferResponse> {
         let buffer = this
-            .update(&mut cx, |this, cx| this.create_buffer(true, cx))
+            .update(&mut cx, |this, cx| this.create_buffer(None, true, cx))
             .await?;
         let peer_id = envelope.original_sender_id()?;
 

crates/project/src/project_tests.rs 🔗

@@ -4152,7 +4152,7 @@ async fn test_save_file_spawns_language_server(cx: &mut gpui::TestAppContext) {
     );
 
     let buffer = project
-        .update(cx, |this, cx| this.create_buffer(false, cx))
+        .update(cx, |this, cx| this.create_buffer(None, false, cx))
         .unwrap()
         .await;
     project.update(cx, |this, cx| {

crates/project/src/worktree_store.rs 🔗

@@ -609,9 +609,7 @@ impl WorktreeStore {
                 scanning_enabled,
                 cx,
             )
-            .await;
-
-            let worktree = worktree?;
+            .await?;
 
             this.update(cx, |this, cx| this.add(&worktree, cx))?;
 

crates/remote/src/remote_client.rs 🔗

@@ -899,6 +899,11 @@ impl RemoteClient {
             .map_or(false, |connection| connection.shares_network_interface())
     }
 
+    pub fn has_wsl_interop(&self) -> bool {
+        self.remote_connection()
+            .map_or(false, |connection| connection.has_wsl_interop())
+    }
+
     pub fn build_command(
         &self,
         program: Option<String>,

crates/remote/src/transport/wsl.rs 🔗

@@ -147,9 +147,14 @@ impl WslRemoteConnection {
     }
 
     async fn run_wsl_command(&self, program: &str, args: &[&str]) -> Result<()> {
-        run_wsl_command_impl(&self.connection_options, program, args, false)
-            .await
-            .map(|_| ())
+        run_wsl_command_impl(wsl_command_impl(
+            &self.connection_options,
+            program,
+            args,
+            false,
+        ))
+        .await
+        .map(|_| ())
     }
 
     async fn ensure_server_binary(
@@ -380,15 +385,13 @@ impl RemoteConnection for WslRemoteConnection {
             let options = self.connection_options.clone();
             async move {
                 let wsl_src = windows_path_to_wsl_path_impl(&options, &src_path).await?;
-
-                run_wsl_command_impl(
+                let command = wsl_command_impl(
                     &options,
                     "cp",
                     &["-r", &wsl_src, &dest_path.to_string()],
                     true,
-                )
-                .await
-                .map_err(|e| {
+                );
+                run_wsl_command_impl(command).await.map_err(|e| {
                     anyhow!(
                         "failed to upload directory {} -> {}: {}",
                         src_path.display(),
@@ -531,17 +534,34 @@ async fn sanitize_path(path: &Path) -> Result<String> {
     Ok(sanitized.replace('\\', "/"))
 }
 
-async fn run_wsl_command_with_output_impl(
+fn run_wsl_command_with_output_impl(
     options: &WslConnectionOptions,
     program: &str,
     args: &[&str],
-) -> Result<String> {
-    match run_wsl_command_impl(options, program, args, true).await {
-        Ok(res) => Ok(res),
-        Err(exec_err) => match run_wsl_command_impl(options, program, args, false).await {
+) -> impl Future<Output = Result<String>> + use<> {
+    let exec_command = wsl_command_impl(options, program, args, true);
+    let command = wsl_command_impl(options, program, args, false);
+    async move {
+        match run_wsl_command_impl(exec_command).await {
             Ok(res) => Ok(res),
-            Err(e) => Err(e.context(exec_err)),
-        },
+            Err(exec_err) => match run_wsl_command_impl(command).await {
+                Ok(res) => Ok(res),
+                Err(e) => Err(e.context(exec_err)),
+            },
+        }
+    }
+}
+
+impl WslConnectionOptions {
+    pub fn abs_windows_path_to_wsl_path(
+        &self,
+        source: &Path,
+    ) -> impl Future<Output = Result<String>> + use<> {
+        let path_str = source.to_string_lossy();
+
+        let source = path_str.strip_prefix(r"\\?\").unwrap_or(&*path_str);
+        let source = source.replace('\\', "/");
+        run_wsl_command_with_output_impl(self, "wslpath", &["-u", &source])
     }
 }
 
@@ -553,27 +573,23 @@ async fn windows_path_to_wsl_path_impl(
     run_wsl_command_with_output_impl(options, "wslpath", &["-u", &source]).await
 }
 
-async fn run_wsl_command_impl(
-    options: &WslConnectionOptions,
-    program: &str,
-    args: &[&str],
-    exec: bool,
-) -> Result<String> {
-    let mut command = wsl_command_impl(options, program, args, exec);
-    let output = command
-        .output()
-        .await
-        .with_context(|| format!("Failed to run command '{:?}'", command))?;
-
-    if !output.status.success() {
-        return Err(anyhow!(
-            "Command '{:?}' failed: {}",
-            command,
-            String::from_utf8_lossy(&output.stderr).trim()
-        ));
-    }
+fn run_wsl_command_impl(mut command: process::Command) -> impl Future<Output = Result<String>> {
+    async move {
+        let output = command
+            .output()
+            .await
+            .with_context(|| format!("Failed to run command '{:?}'", command))?;
+
+        if !output.status.success() {
+            return Err(anyhow!(
+                "Command '{:?}' failed: {}",
+                command,
+                String::from_utf8_lossy(&output.stderr).trim()
+            ));
+        }
 
-    Ok(String::from_utf8_lossy(&output.stdout).trim().to_owned())
+        Ok(String::from_utf8_lossy(&output.stdout).trim().to_owned())
+    }
 }
 
 /// Creates a new `wsl.exe` command that runs the given program with the given arguments.

crates/remote_server/src/headless_project.rs 🔗

@@ -687,9 +687,9 @@ impl HeadlessProject {
     ) -> Result<proto::OpenBufferResponse> {
         let (buffer_store, buffer) = this.update(&mut cx, |this, cx| {
             let buffer_store = this.buffer_store.clone();
-            let buffer = this
-                .buffer_store
-                .update(cx, |buffer_store, cx| buffer_store.create_buffer(true, cx));
+            let buffer = this.buffer_store.update(cx, |buffer_store, cx| {
+                buffer_store.create_buffer(None, true, cx)
+            });
             (buffer_store, buffer)
         });
 

crates/settings_ui/src/settings_ui.rs 🔗

@@ -3206,28 +3206,45 @@ impl SettingsWindow {
                 original_window
                     .update(cx, |workspace, window, cx| {
                         workspace
-                            .with_local_workspace(window, cx, |workspace, window, cx| {
-                                let create_task = workspace.project().update(cx, |project, cx| {
-                                    project.find_or_create_worktree(
-                                        paths::config_dir().as_path(),
-                                        false,
-                                        cx,
-                                    )
-                                });
-                                let open_task = workspace.open_paths(
-                                    vec![paths::settings_file().to_path_buf()],
-                                    OpenOptions {
-                                        visible: Some(OpenVisible::None),
-                                        ..Default::default()
-                                    },
-                                    None,
-                                    window,
-                                    cx,
-                                );
+                            .with_local_or_wsl_workspace(window, cx, |workspace, window, cx| {
+                                let project = workspace.project().clone();
 
                                 cx.spawn_in(window, async move |workspace, cx| {
-                                    create_task.await.ok();
-                                    open_task.await;
+                                    let (config_dir, settings_file) =
+                                        project.update(cx, |project, cx| {
+                                            (
+                                                project.try_windows_path_to_wsl(
+                                                    paths::config_dir().as_path(),
+                                                    cx,
+                                                ),
+                                                project.try_windows_path_to_wsl(
+                                                    paths::settings_file().as_path(),
+                                                    cx,
+                                                ),
+                                            )
+                                        });
+                                    let config_dir = config_dir.await?;
+                                    let settings_file = settings_file.await?;
+                                    project
+                                        .update(cx, |project, cx| {
+                                            project.find_or_create_worktree(&config_dir, false, cx)
+                                        })
+                                        .await
+                                        .ok();
+                                    workspace
+                                        .update_in(cx, |workspace, window, cx| {
+                                            workspace.open_paths(
+                                                vec![settings_file],
+                                                OpenOptions {
+                                                    visible: Some(OpenVisible::None),
+                                                    ..Default::default()
+                                                },
+                                                None,
+                                                window,
+                                                cx,
+                                            )
+                                        })?
+                                        .await;
 
                                     workspace.update_in(cx, |_, window, cx| {
                                         window.activate_window();

crates/util/src/paths.rs 🔗

@@ -361,6 +361,19 @@ impl PathStyle {
         }
     }
 
+    pub fn is_absolute(&self, path_like: &str) -> bool {
+        path_like.starts_with('/')
+            || *self == PathStyle::Windows
+                && (path_like.starts_with('\\')
+                    || path_like
+                        .chars()
+                        .next()
+                        .is_some_and(|c| c.is_ascii_alphabetic())
+                        && path_like[1..]
+                            .strip_prefix(':')
+                            .is_some_and(|path| path.starts_with('/') || path.starts_with('\\')))
+    }
+
     pub fn is_windows(&self) -> bool {
         *self == PathStyle::Windows
     }

crates/workspace/src/workspace.rs 🔗

@@ -2392,7 +2392,7 @@ impl Workspace {
         cx.notify();
     }
 
-    /// Call the given callback with a workspace whose project is local.
+    /// Call the given callback with a workspace whose project is local or remote via WSL (allowing host access).
     ///
     /// If the given workspace has a local project, then it will be passed
     /// to the callback. Otherwise, a new empty window will be created.
@@ -2418,6 +2418,33 @@ impl Workspace {
         }
     }
 
+    /// Call the given callback with a workspace whose project is local or remote via WSL (allowing host access).
+    ///
+    /// If the given workspace has a local project, then it will be passed
+    /// to the callback. Otherwise, a new empty window will be created.
+    pub fn with_local_or_wsl_workspace<T, F>(
+        &mut self,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+        callback: F,
+    ) -> Task<Result<T>>
+    where
+        T: 'static,
+        F: 'static + FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> T,
+    {
+        let project = self.project.read(cx);
+        if project.is_local() || project.is_via_wsl_with_host_interop(cx) {
+            Task::ready(Ok(callback(self, window, cx)))
+        } else {
+            let env = self.project.read(cx).cli_environment(cx);
+            let task = Self::new_local(Vec::new(), self.app_state.clone(), None, env, None, cx);
+            cx.spawn_in(window, async move |_vh, cx| {
+                let (workspace, _) = task.await?;
+                workspace.update(cx, callback)
+            })
+        }
+    }
+
     pub fn worktrees<'a>(&self, cx: &'a App) -> impl 'a + Iterator<Item = Entity<Worktree>> {
         self.project.read(cx).worktrees(cx)
     }
@@ -3075,13 +3102,7 @@ impl Workspace {
         cx.spawn(async move |cx| {
             let (worktree, path) = entry.await?;
             let worktree_id = worktree.read_with(cx, |t, _| t.id());
-            Ok((
-                worktree,
-                ProjectPath {
-                    worktree_id,
-                    path: path,
-                },
-            ))
+            Ok((worktree, ProjectPath { worktree_id, path }))
         })
     }
 
@@ -8231,26 +8252,35 @@ pub fn create_and_open_local_file(
                 .await?;
         }
 
-        let mut items = workspace
+        workspace
             .update_in(cx, |workspace, window, cx| {
-                workspace.with_local_workspace(window, cx, |workspace, window, cx| {
-                    workspace.open_paths(
-                        vec![path.to_path_buf()],
-                        OpenOptions {
-                            visible: Some(OpenVisible::None),
-                            ..Default::default()
-                        },
-                        None,
-                        window,
-                        cx,
-                    )
+                workspace.with_local_or_wsl_workspace(window, cx, |workspace, window, cx| {
+                    let path = workspace
+                        .project
+                        .read_with(cx, |project, cx| project.try_windows_path_to_wsl(path, cx));
+                    cx.spawn_in(window, async move |workspace, cx| {
+                        let path = path.await?;
+                        let mut items = workspace
+                            .update_in(cx, |workspace, window, cx| {
+                                workspace.open_paths(
+                                    vec![path.to_path_buf()],
+                                    OpenOptions {
+                                        visible: Some(OpenVisible::None),
+                                        ..Default::default()
+                                    },
+                                    None,
+                                    window,
+                                    cx,
+                                )
+                            })?
+                            .await;
+                        let item = items.pop().flatten();
+                        item.with_context(|| format!("path {path:?} is not a file"))?
+                    })
                 })
             })?
             .await?
-            .await;
-
-        let item = items.pop().flatten();
-        item.with_context(|| format!("path {path:?} is not a file"))?
+            .await
     })
 }
 

crates/zed/src/main.rs 🔗

@@ -942,16 +942,15 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
                                 serde_json::to_string_pretty(&json_schema_value)
                                     .context("Failed to serialize JSON Schema as JSON")?;
                             let buffer_task = workspace.update(cx, |workspace, cx| {
-                                workspace
-                                    .project()
-                                    .update(cx, |project, cx| project.create_buffer(false, cx))
+                                workspace.project().update(cx, |project, cx| {
+                                    project.create_buffer(json, false, cx)
+                                })
                             })?;
 
                             let buffer = buffer_task.await?;
 
                             workspace.update_in(cx, |workspace, window, cx| {
                                 buffer.update(cx, |buffer, cx| {
-                                    buffer.set_language(json, cx);
                                     buffer.edit([(0..0, json_schema_content)], None, cx);
                                     buffer.edit(
                                         [(0..0, format!("// {} JSON Schema\n", schema_path))],

crates/zed/src/zed.rs 🔗

@@ -80,7 +80,7 @@ use theme::{ActiveTheme, GlobalTheme, SystemAppearance, ThemeRegistry, ThemeSett
 use ui::{PopoverMenuHandle, prelude::*};
 use util::markdown::MarkdownString;
 use util::rel_path::RelPath;
-use util::{ResultExt, asset_str};
+use util::{ResultExt, asset_str, maybe};
 use uuid::Uuid;
 use vim_mode_setting::VimModeSetting;
 use workspace::notifications::{
@@ -1358,98 +1358,112 @@ fn quit(_: &Quit, cx: &mut App) {
 
 fn open_log_file(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
     const MAX_LINES: usize = 1000;
-    workspace
-        .with_local_workspace(window, cx, move |workspace, window, cx| {
-            let app_state = workspace.app_state();
-            let languages = app_state.languages.clone();
-            let fs = app_state.fs.clone();
-            cx.spawn_in(window, async move |workspace, cx| {
-                let (old_log, new_log, log_language) = futures::join!(
-                    fs.load(paths::old_log_file()),
-                    fs.load(paths::log_file()),
-                    languages.language_for_name("log")
-                );
-                let log = match (old_log, new_log) {
-                    (Err(_), Err(_)) => None,
-                    (old_log, new_log) => {
-                        let mut lines = VecDeque::with_capacity(MAX_LINES);
-                        for line in old_log
-                            .iter()
-                            .flat_map(|log| log.lines())
-                            .chain(new_log.iter().flat_map(|log| log.lines()))
-                        {
-                            if lines.len() == MAX_LINES {
-                                lines.pop_front();
-                            }
-                            lines.push_back(line);
+    let app_state = workspace.app_state();
+    let languages = app_state.languages.clone();
+    let fs = app_state.fs.clone();
+    cx.spawn_in(window, async move |workspace, cx| {
+        let log = {
+            let result = futures::join!(
+                fs.load(&paths::old_log_file()),
+                fs.load(&paths::log_file()),
+                languages.language_for_name("log")
+            );
+            match result {
+                (Err(_), Err(e), _) => Err(e),
+                (old_log, new_log, lang) => {
+                    let mut lines = VecDeque::with_capacity(MAX_LINES);
+                    for line in old_log
+                        .iter()
+                        .flat_map(|log| log.lines())
+                        .chain(new_log.iter().flat_map(|log| log.lines()))
+                    {
+                        if lines.len() == MAX_LINES {
+                            lines.pop_front();
                         }
-                        Some(
-                            lines
-                                .into_iter()
-                                .flat_map(|line| [line, "\n"])
-                                .collect::<String>(),
-                        )
+                        lines.push_back(line);
                     }
-                };
-                let log_language = log_language.ok();
+                    Ok((
+                        lines
+                            .into_iter()
+                            .flat_map(|line| [line, "\n"])
+                            .collect::<String>(),
+                        lang.ok(),
+                    ))
+                }
+            }
+        };
 
-                workspace
-                    .update_in(cx, |workspace, window, cx| {
-                        let Some(log) = log else {
-                            struct OpenLogError;
+        let (log, log_language) = match log {
+            Ok((log, log_language)) => (log, log_language),
+            Err(e) => {
+                struct OpenLogError;
 
-                            workspace.show_notification(
-                                NotificationId::unique::<OpenLogError>(),
-                                cx,
-                                |cx| {
-                                    cx.new(|cx| {
-                                        MessageNotification::new(
-                                            format!(
-                                                "Unable to access/open log file at path {:?}",
-                                                paths::log_file().as_path()
-                                            ),
-                                            cx,
-                                        )
-                                    })
-                                },
-                            );
-                            return;
-                        };
-                        let project = workspace.project().clone();
-                        let buffer = project.update(cx, |project, cx| {
-                            project.create_local_buffer(&log, log_language, false, cx)
-                        });
+                workspace
+                    .update(cx, |workspace, cx| {
+                        workspace.show_notification(
+                            NotificationId::unique::<OpenLogError>(),
+                            cx,
+                            |cx| {
+                                cx.new(|cx| {
+                                    MessageNotification::new(
+                                        format!(
+                                            "Unable to access/open log file at path \
+                                                    {}: {e:#}",
+                                            paths::log_file().display()
+                                        ),
+                                        cx,
+                                    )
+                                })
+                            },
+                        );
+                    })
+                    .ok();
+                return;
+            }
+        };
+        maybe!(async move {
+            let project = workspace
+                .read_with(cx, |workspace, _| workspace.project().clone())
+                .ok()?;
+            let buffer = project
+                .update(cx, |project, cx| {
+                    project.create_buffer(log_language, false, cx)
+                })
+                .await
+                .ok()?;
+            buffer.update(cx, |buffer, cx| {
+                buffer.set_capability(Capability::ReadOnly, cx);
+                buffer.set_text(log, cx);
+            });
 
-                        let buffer = cx
-                            .new(|cx| MultiBuffer::singleton(buffer, cx).with_title("Log".into()));
-                        let editor = cx.new(|cx| {
-                            let mut editor =
-                                Editor::for_multibuffer(buffer, Some(project), window, cx);
-                            editor.set_read_only(true);
-                            editor.set_breadcrumb_header(format!(
-                                "Last {} lines in {}",
-                                MAX_LINES,
-                                paths::log_file().display()
-                            ));
-                            editor
-                        });
+            let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx).with_title("Log".into()));
 
-                        editor.update(cx, |editor, cx| {
-                            let last_multi_buffer_offset = editor.buffer().read(cx).len(cx);
-                            editor.change_selections(Default::default(), window, cx, |s| {
-                                s.select_ranges(Some(
-                                    last_multi_buffer_offset..last_multi_buffer_offset,
-                                ));
-                            })
-                        });
+            let editor = cx
+                .new_window_entity(|window, cx| {
+                    let mut editor = Editor::for_multibuffer(buffer, Some(project), window, cx);
+                    editor.set_read_only(true);
+                    editor.set_breadcrumb_header(format!(
+                        "Last {} lines in {}",
+                        MAX_LINES,
+                        paths::log_file().display()
+                    ));
+                    let last_multi_buffer_offset = editor.buffer().read(cx).len(cx);
+                    editor.change_selections(Default::default(), window, cx, |s| {
+                        s.select_ranges(Some(last_multi_buffer_offset..last_multi_buffer_offset));
+                    });
+                    editor
+                })
+                .ok()?;
 
-                        workspace.add_item_to_active_pane(Box::new(editor), None, true, window, cx);
-                    })
-                    .log_err();
-            })
-            .detach();
+            workspace
+                .update_in(cx, |workspace, window, cx| {
+                    workspace.add_item_to_active_pane(Box::new(editor), None, true, window, cx);
+                })
+                .ok()
         })
-        .detach();
+        .await;
+    })
+    .detach();
 }
 
 fn notify_settings_errors(result: settings::SettingsParseResult, is_user: bool, cx: &mut App) {
@@ -1997,33 +2011,39 @@ fn open_bundled_file(
     cx.spawn_in(window, async move |workspace, cx| {
         let language = language.await.log_err();
         workspace
-            .update_in(cx, |workspace, window, cx| {
-                workspace.with_local_workspace(window, cx, |workspace, window, cx| {
-                    let project = workspace.project();
-                    let buffer = project.update(cx, move |project, cx| {
-                        let buffer =
-                            project.create_local_buffer(text.as_ref(), language, false, cx);
-                        buffer.update(cx, |buffer, cx| {
-                            buffer.set_capability(Capability::ReadOnly, cx);
-                        });
-                        buffer
+            .update_in(cx, move |workspace, window, cx| {
+                let project = workspace.project().clone();
+                let buffer = project.update(cx, move |project, cx| {
+                    project.create_buffer(language, false, cx)
+                });
+                cx.spawn_in(window, async move |workspace, cx| {
+                    let buffer = buffer.await?;
+                    buffer.update(cx, |buffer, cx| {
+                        buffer.set_text(text.into_owned(), cx);
+                        buffer.set_capability(Capability::ReadOnly, cx);
                     });
                     let buffer =
                         cx.new(|cx| MultiBuffer::singleton(buffer, cx).with_title(title.into()));
-                    workspace.add_item_to_active_pane(
-                        Box::new(cx.new(|cx| {
-                            let mut editor =
-                                Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
-                            editor.set_read_only(true);
-                            editor.set_should_serialize(false, cx);
-                            editor.set_breadcrumb_header(title.into());
-                            editor
-                        })),
-                        None,
-                        true,
-                        window,
-                        cx,
-                    );
+                    workspace.update_in(cx, |workspace, window, cx| {
+                        workspace.add_item_to_active_pane(
+                            Box::new(cx.new(|cx| {
+                                let mut editor = Editor::for_multibuffer(
+                                    buffer,
+                                    Some(project.clone()),
+                                    window,
+                                    cx,
+                                );
+                                editor.set_read_only(true);
+                                editor.set_should_serialize(false, cx);
+                                editor.set_breadcrumb_header(title.into());
+                                editor
+                            })),
+                            None,
+                            true,
+                            window,
+                            cx,
+                        )
+                    })
                 })
             })?
             .await
@@ -2040,8 +2060,15 @@ fn open_settings_file(
     cx.spawn_in(window, async move |workspace, cx| {
         let (worktree_creation_task, settings_open_task) = workspace
             .update_in(cx, |workspace, window, cx| {
-                workspace.with_local_workspace(window, cx, move |workspace, window, cx| {
-                    let worktree_creation_task = workspace.project().update(cx, |project, cx| {
+                workspace.with_local_or_wsl_workspace(window, cx, move |workspace, window, cx| {
+                    let project = workspace.project().clone();
+
+                    let worktree_creation_task = cx.spawn_in(window, async move |_, cx| {
+                        let config_dir = project
+                            .update(cx, |project, cx| {
+                                project.try_windows_path_to_wsl(paths::config_dir().as_path(), cx)
+                            })
+                            .await?;
                         // Set up a dedicated worktree for settings, since
                         // otherwise we're dropping and re-starting LSP servers
                         // for each file inside on every settings file
@@ -2051,7 +2078,11 @@ fn open_settings_file(
                         // drag and drop from OS) still have their worktrees
                         // released on file close, causing LSP servers'
                         // restarts.
-                        project.find_or_create_worktree(paths::config_dir().as_path(), false, cx)
+                        project
+                            .update(cx, |project, cx| {
+                                project.find_or_create_worktree(&config_dir, false, cx)
+                            })
+                            .await
                     });
                     let settings_open_task =
                         create_and_open_local_file(abs_path, window, cx, default_content);