diff --git a/Cargo.lock b/Cargo.lock index 0d7e250377..1795d3d748 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5527,6 +5527,7 @@ dependencies = [ "futures 0.3.28", "gpui", "language", + "lsp", "node_runtime", "serde", "serde_derive", diff --git a/crates/prettier/Cargo.toml b/crates/prettier/Cargo.toml index ab8d4d9e16..d10e8abdf9 100644 --- a/crates/prettier/Cargo.toml +++ b/crates/prettier/Cargo.toml @@ -10,6 +10,7 @@ path = "src/prettier.rs" language = { path = "../language" } gpui = { path = "../gpui" } fs = { path = "../fs" } +lsp = { path = "../lsp" } node_runtime = { path = "../node_runtime"} util = { path = "../util" } diff --git a/crates/prettier/src/prettier.rs b/crates/prettier/src/prettier.rs index e5b001f226..fc7736b0e0 100644 --- a/crates/prettier/src/prettier.rs +++ b/crates/prettier/src/prettier.rs @@ -4,12 +4,15 @@ use std::sync::Arc; use anyhow::Context; use fs::Fs; -use gpui::ModelHandle; +use gpui::{AsyncAppContext, ModelHandle, Task}; use language::{Buffer, Diff}; +use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId}; use node_runtime::NodeRuntime; +use serde::{Deserialize, Serialize}; +use util::paths::DEFAULT_PRETTIER_DIR; pub struct Prettier { - _private: (), + server: Arc, } #[derive(Debug)] @@ -18,7 +21,9 @@ pub struct LocateStart { pub starting_path: Arc, } +pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js"; pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js"); +const PRETTIER_PACKAGE_NAME: &str = "prettier"; impl Prettier { // This was taken from the prettier-vscode extension. @@ -141,16 +146,55 @@ impl Prettier { } } - pub async fn start(prettier_dir: &Path, node: Arc) -> anyhow::Result { - anyhow::ensure!( - prettier_dir.is_dir(), - "Prettier dir {prettier_dir:?} is not a directory" - ); - anyhow::bail!("TODO kb: start prettier server in {prettier_dir:?}") + pub fn start( + prettier_dir: PathBuf, + node: Arc, + cx: AsyncAppContext, + ) -> Task> { + cx.spawn(|cx| async move { + anyhow::ensure!( + prettier_dir.is_dir(), + "Prettier dir {prettier_dir:?} is not a directory" + ); + let prettier_server = DEFAULT_PRETTIER_DIR.join(PRETTIER_SERVER_FILE); + anyhow::ensure!( + prettier_server.is_file(), + "no prettier server package found at {prettier_server:?}" + ); + + let node_path = node.binary_path().await?; + let server = LanguageServer::new( + LanguageServerId(0), + LanguageServerBinary { + path: node_path, + arguments: vec![prettier_server.into(), prettier_dir.into()], + }, + Path::new("/"), + None, + cx, + ) + .context("prettier server creation")?; + let server = server + .initialize(None) + .await + .context("prettier server initialization")?; + Ok(Self { server }) + }) } - pub async fn format(&self, buffer: &ModelHandle) -> anyhow::Result { - todo!() + pub async fn format( + &self, + buffer: &ModelHandle, + cx: &AsyncAppContext, + ) -> anyhow::Result { + let buffer_text = buffer.read_with(cx, |buffer, _| buffer.text()); + let response = self + .server + .request::(PrettierFormatParams { text: buffer_text }) + .await + .context("prettier format request")?; + dbg!("Formatted text", response.text); + anyhow::bail!("TODO kb calculate the diff") } pub async fn clear_cache(&self) -> anyhow::Result<()> { @@ -158,7 +202,6 @@ impl Prettier { } } -const PRETTIER_PACKAGE_NAME: &str = "prettier"; async fn find_closest_prettier_dir( paths_to_check: Vec, fs: &dyn Fs, @@ -206,3 +249,23 @@ async fn find_closest_prettier_dir( } Ok(None) } + +enum PrettierFormat {} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct PrettierFormatParams { + text: String, +} + +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct PrettierFormatResult { + text: String, +} + +impl lsp::request::Request for PrettierFormat { + type Params = PrettierFormatParams; + type Result = PrettierFormatResult; + const METHOD: &'static str = "prettier/format"; +} diff --git a/crates/prettier/src/prettier_server.js b/crates/prettier/src/prettier_server.js index d232ac9efe..0caa45b917 100644 --- a/crates/prettier/src/prettier_server.js +++ b/crates/prettier/src/prettier_server.js @@ -5,17 +5,17 @@ const { once } = require('events'); const prettierContainerPath = process.argv[2]; if (prettierContainerPath == null || prettierContainerPath.length == 0) { - console.error(`Prettier path argument was not specified or empty.\nUsage: ${process.argv[0]} ${process.argv[1]} prettier/path`); + sendResponse(makeError(`Prettier path argument was not specified or empty.\nUsage: ${process.argv[0]} ${process.argv[1]} prettier/path`)); process.exit(1); } fs.stat(prettierContainerPath, (err, stats) => { if (err) { - console.error(`Path '${prettierContainerPath}' does not exist.`); + sendResponse(makeError(`Path '${prettierContainerPath}' does not exist.`)); process.exit(1); } if (!stats.isDirectory()) { - console.log(`Path '${prettierContainerPath}' exists but is not a directory.`); + sendResponse(makeError(`Path '${prettierContainerPath}' exists but is not a directory.`)); process.exit(1); } }); @@ -26,19 +26,19 @@ const prettierPath = path.join(prettierContainerPath, 'node_modules/prettier'); let prettier; try { prettier = await loadPrettier(prettierPath); - } catch (error) { - console.error("Failed to load prettier: ", error); + } catch (e) { + sendResponse(makeError(`Failed to load prettier: ${e}`)); process.exit(1); } - console.log("Prettier loadded successfully."); + sendResponse(makeError("Prettier loadded successfully.")); process.stdin.resume(); handleBuffer(prettier); })() async function handleBuffer(prettier) { for await (let messageText of readStdin()) { - handleData(messageText, prettier).catch(e => { - console.error("Failed to handle formatter request", e); + handleMessage(messageText, prettier).catch(e => { + sendResponse(makeError(`error during message handling: ${e}`)); }); } } @@ -107,43 +107,63 @@ async function* readStdin() { yield message.toString('utf8'); } } catch (e) { - console.error(`Error reading stdin: ${e}`); + sendResponse(makeError(`Error reading stdin: ${e}`)); } finally { process.stdin.off('data', () => { }); } } -async function handleData(messageText, prettier) { - try { - const message = JSON.parse(messageText); - await handleMessage(prettier, message); - } catch (e) { - sendResponse(makeError(`Request JSON parse error: ${e}`)); +// ? +// shutdown +// error +async function handleMessage(messageText, prettier) { + const message = JSON.parse(messageText); + const { method, id, params } = message; + if (method === undefined) { + throw new Error(`Message method is undefined: ${messageText}`); + } + if (id === undefined) { + throw new Error(`Message id is undefined: ${messageText}`); + } + + if (method === 'prettier/format') { + if (params === undefined || params.text === undefined) { + throw new Error(`Message params.text is undefined: ${messageText}`); + } + let formattedText = await prettier.format(params.text); + sendResponse({ id, result: { text: formattedText } }); + } else if (method === 'prettier/clear_cache') { + prettier.clearConfigCache(); + sendResponse({ id, result: null }); + } else if (method === 'initialize') { + sendResponse({ + id, + result: { + "capabilities": {} + } + }); + } else { + throw new Error(`Unknown method: ${method}`); } } -// format -// clear_cache -// -// shutdown -// error - -async function handleMessage(prettier, message) { - // TODO kb handle message.method, message.params and message.id - console.log(`message: ${JSON.stringify(message)}`); - sendResponse({ method: "hi", result: null }); -} - function makeError(message) { - return { method: "error", message }; + return { + error: { + "code": -32600, // invalid request code + message, + } + }; } function sendResponse(response) { - const message = Buffer.from(JSON.stringify(response)); - const length = Buffer.alloc(4); - length.writeUInt32LE(message.length); - process.stdout.write(length); - process.stdout.write(message); + let responsePayloadString = JSON.stringify({ + jsonrpc: "2.0", + ...response + }); + let headers = `Content-Length: ${Buffer.byteLength(responsePayloadString)}\r\n\r\n`; + let dataToSend = headers + responsePayloadString; + process.stdout.write(dataToSend); } function loadPrettier(prettierPath) { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index b3d00afd8a..84b5f09cac 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -53,7 +53,7 @@ use lsp::{ use lsp_command::*; use node_runtime::NodeRuntime; use postage::watch; -use prettier::{LocateStart, Prettier}; +use prettier::{LocateStart, Prettier, PRETTIER_SERVER_FILE, PRETTIER_SERVER_JS}; use project_settings::{LspSettings, ProjectSettings}; use rand::prelude::*; use search::SearchQuery; @@ -4138,7 +4138,7 @@ impl Project { Ok(prettier) => { format_operation = Some(FormatOperation::Prettier( prettier - .format(buffer) + .format(buffer, &cx) .await .context("formatting via prettier")?, )); @@ -4176,7 +4176,7 @@ impl Project { Ok(prettier) => { format_operation = Some(FormatOperation::Prettier( prettier - .format(buffer) + .format(buffer, &cx) .await .context("formatting via prettier")?, )); @@ -8283,18 +8283,12 @@ impl Project { return existing_prettier; } - let task_prettier_dir = prettier_dir.clone(); + let start_task = Prettier::start(prettier_dir.clone(), node, cx.clone()); let new_prettier_task = cx .background() .spawn(async move { - Ok(Arc::new( - Prettier::start(&task_prettier_dir, node) - .await - .with_context(|| { - format!("starting new prettier for path {task_prettier_dir:?}") - })?, - )) - .map_err(Arc::new) + Ok(Arc::new(start_task.await.context("starting new prettier")?)) + .map_err(Arc::new) }) .shared(); this.update(&mut cx, |project, _| { @@ -8344,16 +8338,17 @@ impl Project { .get(&(worktree, default_prettier_dir.to_path_buf())) { // TODO kb need to compare plugins, install missing and restart prettier + // TODO kb move the entire prettier init logic into prettier.rs return; } let fs = Arc::clone(&self.fs); cx.background() .spawn(async move { - let prettier_wrapper_path = default_prettier_dir.join("prettier_server.js"); + let prettier_wrapper_path = default_prettier_dir.join(PRETTIER_SERVER_FILE); // method creates parent directory if it doesn't exist - fs.save(&prettier_wrapper_path, &Rope::from(prettier::PRETTIER_SERVER_JS), LineEnding::Unix).await - .with_context(|| format!("writing prettier_server.js file at {prettier_wrapper_path:?}"))?; + fs.save(&prettier_wrapper_path, &Rope::from(PRETTIER_SERVER_JS), LineEnding::Unix).await + .with_context(|| format!("writing {PRETTIER_SERVER_FILE} file at {prettier_wrapper_path:?}"))?; let packages_to_versions = future::try_join_all( prettier_plugins