Draft local and remote prettier separation

Kirill Bulatov created

Change summary

Cargo.lock                             |   1 
crates/prettier/Cargo.toml             |   1 
crates/prettier/src/prettier.rs        |  58 +++
crates/prettier/src/prettier_server.js |   3 
crates/project/src/project.rs          | 360 ++++++++++++++++++---------
crates/rpc/proto/zed.proto             |  29 ++
crates/rpc/src/proto.rs                |  12 
7 files changed, 328 insertions(+), 136 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -5523,6 +5523,7 @@ name = "prettier"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "client",
  "collections",
  "fs",
  "futures 0.3.28",

crates/prettier/Cargo.toml 🔗

@@ -7,6 +7,7 @@ edition = "2021"
 path = "src/prettier.rs"
 
 [dependencies]
+client = { path = "../client" }
 collections = { path = "../collections"}
 language = { path = "../language" }
 gpui = { path = "../gpui" }

crates/prettier/src/prettier.rs 🔗

@@ -3,6 +3,7 @@ use std::path::{Path, PathBuf};
 use std::sync::Arc;
 
 use anyhow::Context;
+use client::Client;
 use collections::{HashMap, HashSet};
 use fs::Fs;
 use gpui::{AsyncAppContext, ModelHandle};
@@ -13,13 +14,24 @@ use node_runtime::NodeRuntime;
 use serde::{Deserialize, Serialize};
 use util::paths::DEFAULT_PRETTIER_DIR;
 
-pub struct Prettier {
+pub enum Prettier {
+    Local(Local),
+    Remote(Remote),
+}
+
+pub struct Local {
     worktree_id: Option<usize>,
     default: bool,
     prettier_dir: PathBuf,
     server: Arc<LanguageServer>,
 }
 
+pub struct Remote {
+    worktree_id: Option<usize>,
+    prettier_dir: PathBuf,
+    client: Arc<Client>,
+}
+
 #[derive(Debug)]
 pub struct LocateStart {
     pub worktree_root_path: Arc<Path>,
@@ -48,6 +60,14 @@ impl Prettier {
         ".editorconfig",
     ];
 
+    pub fn remote(worktree_id: Option<usize>, prettier_dir: PathBuf, client: Arc<Client>) -> Self {
+        Self::Remote(Remote {
+            worktree_id,
+            prettier_dir,
+            client,
+        })
+    }
+
     pub async fn locate(
         starting_path: Option<LocateStart>,
         fs: Arc<dyn Fs>,
@@ -188,12 +208,12 @@ impl Prettier {
             .spawn(server.initialize(None))
             .await
             .context("prettier server initialization")?;
-        Ok(Self {
+        Ok(Self::Local(Local {
             worktree_id,
             server,
             default: prettier_dir == DEFAULT_PRETTIER_DIR.as_path(),
             prettier_dir,
-        })
+        }))
     }
 
     pub async fn format(
@@ -239,7 +259,7 @@ impl Prettier {
                 log::warn!("Found multiple parsers with plugins {parsers_with_plugins:?}, will select only one: {selected_parser_with_plugins:?}");
             }
 
-            let prettier_node_modules = self.prettier_dir.join("node_modules");
+            let prettier_node_modules = self.prettier_dir().join("node_modules");
             anyhow::ensure!(prettier_node_modules.is_dir(), "Prettier node_modules dir does not exist: {prettier_node_modules:?}");
             let plugin_name_into_path = |plugin_name: &str| {
                 let prettier_plugin_dir = prettier_node_modules.join(plugin_name);
@@ -278,7 +298,7 @@ impl Prettier {
                 None => (None, Vec::new()),
             };
 
-            let prettier_options = if self.default {
+            let prettier_options = if self.is_default() {
                 let language_settings = language_settings(buffer_language, buffer.file(), cx);
                 let mut options = language_settings.prettier.clone();
                 if !options.contains_key("tabWidth") {
@@ -323,7 +343,8 @@ impl Prettier {
             })
         }).context("prettier params calculation")?;
         let response = self
-            .server
+            .server()
+            .expect("TODO kb split into local and remote")
             .request::<Format>(params)
             .await
             .context("prettier format request")?;
@@ -332,26 +353,39 @@ impl Prettier {
     }
 
     pub async fn clear_cache(&self) -> anyhow::Result<()> {
-        self.server
+        self.server()
+            .expect("TODO kb split into local and remote")
             .request::<ClearCache>(())
             .await
             .context("prettier clear cache")
     }
 
-    pub fn server(&self) -> &Arc<LanguageServer> {
-        &self.server
+    pub fn server(&self) -> Option<&Arc<LanguageServer>> {
+        match self {
+            Prettier::Local(local) => Some(&local.server),
+            Prettier::Remote(_) => None,
+        }
     }
 
     pub fn is_default(&self) -> bool {
-        self.default
+        match self {
+            Prettier::Local(local) => local.default,
+            Prettier::Remote(_) => false,
+        }
     }
 
     pub fn prettier_dir(&self) -> &Path {
-        &self.prettier_dir
+        match self {
+            Prettier::Local(local) => &local.prettier_dir,
+            Prettier::Remote(remote) => &remote.prettier_dir,
+        }
     }
 
     pub fn worktree_id(&self) -> Option<usize> {
-        self.worktree_id
+        match self {
+            Prettier::Local(local) => local.worktree_id,
+            Prettier::Remote(remote) => remote.worktree_id,
+        }
     }
 }
 

crates/prettier/src/prettier_server.js 🔗

@@ -130,9 +130,6 @@ async function* readStdin() {
     }
 }
 
-// TODO kb, more methods?
-// shutdown
-// error
 async function handleMessage(message, prettier) {
     const { method, id, params } = message;
     if (method === undefined) {

crates/project/src/project.rs 🔗

@@ -613,6 +613,8 @@ impl Project {
         client.add_model_request_handler(Self::handle_open_buffer_by_path);
         client.add_model_request_handler(Self::handle_save_buffer);
         client.add_model_message_handler(Self::handle_update_diff_base);
+        client.add_model_request_handler(Self::handle_prettier_instance_for_buffer);
+        client.add_model_request_handler(Self::handle_invoke_prettier);
     }
 
     pub fn local(
@@ -4124,10 +4126,8 @@ impl Project {
                             if let Some(prettier_task) = this
                                 .update(&mut cx, |project, cx| {
                                     project.prettier_instance_for_buffer(buffer, cx)
-                                }) {
-                                    match prettier_task
-                                        .await
-                                        .await
+                                }).await {
+                                    match prettier_task.await
                                     {
                                         Ok(prettier) => {
                                             let buffer_path = buffer.read_with(&cx, |buffer, cx| {
@@ -4165,10 +4165,8 @@ impl Project {
                             if let Some(prettier_task) = this
                                 .update(&mut cx, |project, cx| {
                                     project.prettier_instance_for_buffer(buffer, cx)
-                                }) {
-                                    match prettier_task
-                                        .await
-                                        .await
+                                }).await {
+                                    match prettier_task.await
                                     {
                                         Ok(prettier) => {
                                             let buffer_path = buffer.read_with(&cx, |buffer, cx| {
@@ -8288,143 +8286,269 @@ impl Project {
         }
     }
 
+    async fn handle_prettier_instance_for_buffer(
+        this: ModelHandle<Self>,
+        envelope: TypedEnvelope<proto::PrettierInstanceForBuffer>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> anyhow::Result<proto::PrettierInstanceForBufferResponse> {
+        let prettier_instance_for_buffer_task = this.update(&mut cx, |this, cx| {
+            let buffer = this
+                .opened_buffers
+                .get(&envelope.payload.buffer_id)
+                .and_then(|buffer| buffer.upgrade(cx))
+                .with_context(|| format!("unknown buffer id {}", envelope.payload.buffer_id))?;
+            anyhow::Ok(this.prettier_instance_for_buffer(&buffer, cx))
+        })?;
+
+        let prettier_path = match prettier_instance_for_buffer_task.await {
+            Some(prettier) => match prettier.await {
+                Ok(prettier) => Some(prettier.prettier_dir().display().to_string()),
+                Err(e) => {
+                    anyhow::bail!("Failed to create prettier instance for remote request: {e:#}")
+                }
+            },
+            None => None,
+        };
+        Ok(proto::PrettierInstanceForBufferResponse { prettier_path })
+    }
+
+    async fn handle_invoke_prettier(
+        this: ModelHandle<Self>,
+        envelope: TypedEnvelope<proto::InvokePrettierForBuffer>,
+        _: Arc<Client>,
+        mut cx: AsyncAppContext,
+    ) -> anyhow::Result<proto::InvokePrettierForBufferResponse> {
+        let prettier = this
+            .read_with(&cx, |this, _| {
+                this.prettier_instances
+                    .get(&(
+                        envelope.payload.worktree_id.map(WorktreeId::from_proto),
+                        PathBuf::from(&envelope.payload.buffer_path),
+                    ))
+                    .cloned()
+            })
+            .with_context(|| {
+                format!(
+                    "Missing prettier for worktree {:?} and path {}",
+                    envelope.payload.worktree_id, envelope.payload.buffer_path,
+                )
+            })?
+            .await;
+        let prettier = match prettier {
+            Ok(prettier) => prettier,
+            Err(e) => anyhow::bail!("Prettier instance failed to start: {e:#}"),
+        };
+
+        let buffer = this
+            .update(&mut cx, |this, cx| {
+                this.opened_buffers
+                    .get(&envelope.payload.buffer_id)
+                    .and_then(|buffer| buffer.upgrade(cx))
+            })
+            .with_context(|| format!("unknown buffer id {}", envelope.payload.buffer_id))?;
+
+        let buffer_path = buffer.read_with(&cx, |buffer, cx| {
+            File::from_dyn(buffer.file()).map(|f| f.full_path(cx))
+        });
+
+        let diff = prettier
+            .format(&buffer, buffer_path, &cx)
+            .await
+            .context("handle buffer formatting")?;
+        todo!("TODO kb serialize diff")
+    }
+
     fn prettier_instance_for_buffer(
         &mut self,
         buffer: &ModelHandle<Buffer>,
         cx: &mut ModelContext<Self>,
-    ) -> Option<Task<Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>>> {
+    ) -> Task<Option<Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>>> {
         let buffer = buffer.read(cx);
         let buffer_file = buffer.file();
-        let buffer_language = buffer.language()?;
+        let Some(buffer_language) = buffer.language() else {
+            return Task::ready(None);
+        };
         if !buffer_language
             .lsp_adapters()
             .iter()
             .flat_map(|adapter| adapter.enabled_formatters())
             .any(|formatter| matches!(formatter, BundledFormatter::Prettier { .. }))
         {
-            return None;
+            return Task::ready(None);
         }
 
-        let node = Arc::clone(self.node.as_ref()?);
         let buffer_file = File::from_dyn(buffer_file);
         let buffer_path = buffer_file.map(|file| Arc::clone(file.path()));
         let worktree_path = buffer_file
             .as_ref()
-            .map(|file| file.worktree.read(cx).abs_path());
+            .and_then(|file| Some(file.worktree.read(cx).abs_path()));
         let worktree_id = buffer_file.map(|file| file.worktree_id(cx));
+        if self.is_local() || worktree_id.is_none() || worktree_path.is_none() {
+            let Some(node) = self.node.as_ref().map(Arc::clone) else {
+                return Task::ready(None);
+            };
+            let task = cx.spawn(|this, mut cx| async move {
+                let fs = this.update(&mut cx, |project, _| Arc::clone(&project.fs));
+                let prettier_dir = match cx
+                    .background()
+                    .spawn(Prettier::locate(
+                        worktree_path.zip(buffer_path).map(
+                            |(worktree_root_path, starting_path)| LocateStart {
+                                worktree_root_path,
+                                starting_path,
+                            },
+                        ),
+                        fs,
+                    ))
+                    .await
+                {
+                    Ok(path) => path,
+                    Err(e) => {
+                        return Some(
+                            Task::ready(Err(Arc::new(e.context(
+                                "determining prettier path for worktree {worktree_path:?}",
+                            ))))
+                            .shared(),
+                        );
+                    }
+                };
 
-        let task = cx.spawn(|this, mut cx| async move {
-            let fs = this.update(&mut cx, |project, _| Arc::clone(&project.fs));
-            let prettier_dir = match cx
-                .background()
-                .spawn(Prettier::locate(
-                    worktree_path
-                        .zip(buffer_path)
-                        .map(|(worktree_root_path, starting_path)| LocateStart {
-                            worktree_root_path,
-                            starting_path,
-                        }),
-                    fs,
-                ))
-                .await
-            {
-                Ok(path) => path,
-                Err(e) => {
-                    return Task::Ready(Some(Result::Err(Arc::new(
-                        e.context("determining prettier path for worktree {worktree_path:?}"),
-                    ))))
-                    .shared();
+                if let Some(existing_prettier) = this.update(&mut cx, |project, _| {
+                    project
+                        .prettier_instances
+                        .get(&(worktree_id, prettier_dir.clone()))
+                        .cloned()
+                }) {
+                    return Some(existing_prettier);
                 }
-            };
 
-            if let Some(existing_prettier) = this.update(&mut cx, |project, _| {
-                project
-                    .prettier_instances
-                    .get(&(worktree_id, prettier_dir.clone()))
-                    .cloned()
-            }) {
-                return existing_prettier;
-            }
+                log::info!("Found prettier at {prettier_dir:?}, starting.");
+                let task_prettier_dir = prettier_dir.clone();
+                let weak_project = this.downgrade();
+                let new_server_id =
+                    this.update(&mut cx, |this, _| this.languages.next_language_server_id());
+                let new_prettier_task = cx
+                    .spawn(|mut cx| async move {
+                        let prettier = Prettier::start(
+                            worktree_id.map(|id| id.to_usize()),
+                            new_server_id,
+                            task_prettier_dir,
+                            node,
+                            cx.clone(),
+                        )
+                        .await
+                        .context("prettier start")
+                        .map_err(Arc::new)?;
+                        log::info!("Had started prettier in {:?}", prettier.prettier_dir());
 
-            log::info!("Found prettier at {prettier_dir:?}, starting.");
-            let task_prettier_dir = prettier_dir.clone();
-            let weak_project = this.downgrade();
-            let new_server_id =
-                this.update(&mut cx, |this, _| this.languages.next_language_server_id());
-            let new_prettier_task = cx
-                .spawn(|mut cx| async move {
-                    let prettier = Prettier::start(
-                        worktree_id.map(|id| id.to_usize()),
-                        new_server_id,
-                        task_prettier_dir,
-                        node,
-                        cx.clone(),
-                    )
-                    .await
-                    .context("prettier start")
-                    .map_err(Arc::new)?;
-                    log::info!("Had started prettier in {:?}", prettier.prettier_dir());
-
-                    if let Some(project) = weak_project.upgrade(&mut cx) {
-                        project.update(&mut cx, |project, cx| {
-                            let name = if prettier.is_default() {
-                                LanguageServerName(Arc::from("prettier (default)"))
-                            } else {
-                                let prettier_dir = prettier.prettier_dir();
-                                let worktree_path = prettier
-                                    .worktree_id()
-                                    .map(WorktreeId::from_usize)
-                                    .and_then(|id| project.worktree_for_id(id, cx))
-                                    .map(|worktree| worktree.read(cx).abs_path());
-                                match worktree_path {
-                                    Some(worktree_path) => {
-                                        if worktree_path.as_ref() == prettier_dir {
-                                            LanguageServerName(Arc::from(format!(
-                                                "prettier ({})",
-                                                prettier_dir
-                                                    .file_name()
-                                                    .and_then(|name| name.to_str())
-                                                    .unwrap_or_default()
-                                            )))
-                                        } else {
-                                            let dir_to_display = match prettier_dir
-                                                .strip_prefix(&worktree_path)
-                                                .ok()
-                                            {
-                                                Some(relative_path) => relative_path,
-                                                None => prettier_dir,
-                                            };
-                                            LanguageServerName(Arc::from(format!(
-                                                "prettier ({})",
-                                                dir_to_display.display(),
-                                            )))
+                        if let Some((project, prettier_server)) =
+                            weak_project.upgrade(&mut cx).zip(prettier.server())
+                        {
+                            project.update(&mut cx, |project, cx| {
+                                let name = if prettier.is_default() {
+                                    LanguageServerName(Arc::from("prettier (default)"))
+                                } else {
+                                    let prettier_dir = prettier.prettier_dir();
+                                    let worktree_path = prettier
+                                        .worktree_id()
+                                        .map(WorktreeId::from_usize)
+                                        .and_then(|id| project.worktree_for_id(id, cx))
+                                        .map(|worktree| worktree.read(cx).abs_path());
+                                    match worktree_path {
+                                        Some(worktree_path) => {
+                                            if worktree_path.as_ref() == prettier_dir {
+                                                LanguageServerName(Arc::from(format!(
+                                                    "prettier ({})",
+                                                    prettier_dir
+                                                        .file_name()
+                                                        .and_then(|name| name.to_str())
+                                                        .unwrap_or_default()
+                                                )))
+                                            } else {
+                                                let dir_to_display = match prettier_dir
+                                                    .strip_prefix(&worktree_path)
+                                                    .ok()
+                                                {
+                                                    Some(relative_path) => relative_path,
+                                                    None => prettier_dir,
+                                                };
+                                                LanguageServerName(Arc::from(format!(
+                                                    "prettier ({})",
+                                                    dir_to_display.display(),
+                                                )))
+                                            }
                                         }
+                                        None => LanguageServerName(Arc::from(format!(
+                                            "prettier ({})",
+                                            prettier_dir.display(),
+                                        ))),
                                     }
-                                    None => LanguageServerName(Arc::from(format!(
-                                        "prettier ({})",
-                                        prettier_dir.display(),
-                                    ))),
-                                }
-                            };
-                            project
-                                .supplementary_language_servers
-                                .insert(new_server_id, (name, Arc::clone(prettier.server())));
-                            // TODO kb could there be a race with multiple default prettier instances added?
-                            // also, clean up prettiers for dropped workspaces (e.g. external files that got closed)
-                            cx.emit(Event::LanguageServerAdded(new_server_id));
-                        });
+                                };
+
+                                project
+                                    .supplementary_language_servers
+                                    .insert(new_server_id, (name, Arc::clone(prettier_server)));
+                                // TODO kb could there be a race with multiple default prettier instances added?
+                                // also, clean up prettiers for dropped workspaces (e.g. external files that got closed)
+                                cx.emit(Event::LanguageServerAdded(new_server_id));
+                            });
+                        }
+                        Ok(Arc::new(prettier)).map_err(Arc::new)
+                    })
+                    .shared();
+                this.update(&mut cx, |project, _| {
+                    project
+                        .prettier_instances
+                        .insert((worktree_id, prettier_dir), new_prettier_task.clone());
+                });
+                Some(new_prettier_task)
+            });
+            task
+        } else if let Some(project_id) = self.remote_id() {
+            let client = self.client.clone();
+            let request = proto::PrettierInstanceForBuffer {
+                project_id,
+                buffer_id: buffer.remote_id(),
+            };
+            let task = cx.spawn(|this, mut cx| async move {
+                match client.request(request).await {
+                    Ok(response) => {
+                        response
+                            .prettier_path
+                            .map(PathBuf::from)
+                            .map(|prettier_path| {
+                                let prettier_task = Task::ready(
+                                    Ok(Arc::new(Prettier::remote(
+                                        worktree_id.map(|id| id.to_usize()),
+                                        prettier_path.clone(),
+                                        client,
+                                    )))
+                                    .map_err(Arc::new),
+                                )
+                                .shared();
+                                this.update(&mut cx, |project, _| {
+                                    project.prettier_instances.insert(
+                                        (worktree_id, prettier_path),
+                                        prettier_task.clone(),
+                                    );
+                                });
+                                prettier_task
+                            })
                     }
-                    anyhow::Ok(Arc::new(prettier)).map_err(Arc::new)
-                })
-                .shared();
-            this.update(&mut cx, |project, _| {
-                project
-                    .prettier_instances
-                    .insert((worktree_id, prettier_dir), new_prettier_task.clone());
+                    Err(e) => {
+                        log::error!("Prettier init remote request failed: {e:#}");
+                        None
+                    }
+                }
             });
-            new_prettier_task
-        });
-        Some(task)
+
+            task
+        } else {
+            Task::ready(Some(
+                Task::ready(Err(Arc::new(anyhow!("project does not have a remote id")))).shared(),
+            ))
+        }
     }
 
     fn install_default_formatters(

crates/rpc/proto/zed.proto 🔗

@@ -170,7 +170,12 @@ message Envelope {
 
         LinkChannel link_channel = 140;
         UnlinkChannel unlink_channel = 141;
-        MoveChannel move_channel = 142; // current max: 144
+        MoveChannel move_channel = 142;
+
+        PrettierInstanceForBuffer prettier_instance_for_buffer = 145;
+        PrettierInstanceForBufferResponse prettier_instance_for_buffer_response = 146;
+        InvokePrettierForBuffer invoke_prettier_for_buffer = 147;
+        InvokePrettierForBufferResponse invoke_prettier_for_buffer_response = 148;  // Current max: 148
     }
 }
 
@@ -1557,3 +1562,25 @@ message UpdateDiffBase {
     uint64 buffer_id = 2;
     optional string diff_base = 3;
 }
+
+message PrettierInstanceForBuffer {
+    uint64 project_id = 1;
+    uint64 buffer_id = 2;
+}
+
+message PrettierInstanceForBufferResponse {
+    optional string prettier_path = 1;
+}
+
+message InvokePrettierForBuffer {
+    uint64 project_id = 1;
+    string buffer_path = 2;
+    uint64 buffer_id = 3;
+    optional uint64 worktree_id = 4;
+    string method = 5;
+    optional string command_parameters = 6;
+}
+
+message InvokePrettierForBufferResponse {
+    optional string diff = 1;
+}

crates/rpc/src/proto.rs 🔗

@@ -273,6 +273,10 @@ messages!(
     (UpdateChannelBufferCollaborators, Foreground),
     (AckBufferOperation, Background),
     (AckChannelMessage, Background),
+    (PrettierInstanceForBuffer, Background),
+    (InvokePrettierForBuffer, Background),
+    (PrettierInstanceForBufferResponse, Background),
+    (InvokePrettierForBufferResponse, Background),
 );
 
 request_messages!(
@@ -349,7 +353,9 @@ request_messages!(
     (UpdateProject, Ack),
     (UpdateWorktree, Ack),
     (JoinChannelBuffer, JoinChannelBufferResponse),
-    (LeaveChannelBuffer, Ack)
+    (LeaveChannelBuffer, Ack),
+    (PrettierInstanceForBuffer, PrettierInstanceForBufferResponse),
+    (InvokePrettierForBuffer, InvokePrettierForBufferResponse),
 );
 
 entity_messages!(
@@ -400,7 +406,9 @@ entity_messages!(
     UpdateProjectCollaborator,
     UpdateWorktree,
     UpdateWorktreeSettings,
-    UpdateDiffBase
+    UpdateDiffBase,
+    PrettierInstanceForBuffer,
+    InvokePrettierForBuffer,
 );
 
 entity_messages!(