prettier.rs

  1use std::collections::VecDeque;
  2use std::path::{Path, PathBuf};
  3use std::sync::Arc;
  4
  5use anyhow::Context;
  6use client::{proto, Client};
  7use collections::{HashMap, HashSet};
  8use fs::Fs;
  9use gpui::{AsyncAppContext, ModelHandle};
 10use language::language_settings::language_settings;
 11use language::proto::deserialize_diff;
 12use language::{Buffer, BundledFormatter, Diff};
 13use lsp::request::Request;
 14use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId};
 15use node_runtime::NodeRuntime;
 16use serde::{Deserialize, Serialize};
 17use util::paths::DEFAULT_PRETTIER_DIR;
 18
 19pub enum Prettier {
 20    Local(Local),
 21    Remote(Remote),
 22}
 23
 24pub struct Local {
 25    worktree_id: Option<usize>,
 26    default: bool,
 27    prettier_dir: PathBuf,
 28    server: Arc<LanguageServer>,
 29}
 30
 31pub struct Remote {
 32    project_id: u64,
 33    worktree_id: Option<usize>,
 34    prettier_dir: PathBuf,
 35    client: Arc<Client>,
 36}
 37
 38#[derive(Debug)]
 39pub struct LocateStart {
 40    pub worktree_root_path: Arc<Path>,
 41    pub starting_path: Arc<Path>,
 42}
 43
 44pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js";
 45pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js");
 46const PRETTIER_PACKAGE_NAME: &str = "prettier";
 47const TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME: &str = "prettier-plugin-tailwindcss";
 48
 49impl Prettier {
 50    // This was taken from the prettier-vscode extension.
 51    pub const CONFIG_FILE_NAMES: &'static [&'static str] = &[
 52        ".prettierrc",
 53        ".prettierrc.json",
 54        ".prettierrc.json5",
 55        ".prettierrc.yaml",
 56        ".prettierrc.yml",
 57        ".prettierrc.toml",
 58        ".prettierrc.js",
 59        ".prettierrc.cjs",
 60        "package.json",
 61        "prettier.config.js",
 62        "prettier.config.cjs",
 63        ".editorconfig",
 64    ];
 65
 66    pub fn remote(
 67        project_id: u64,
 68        worktree_id: Option<usize>,
 69        prettier_dir: PathBuf,
 70        client: Arc<Client>,
 71    ) -> Self {
 72        Self::Remote(Remote {
 73            project_id,
 74            worktree_id,
 75            prettier_dir,
 76            client,
 77        })
 78    }
 79
 80    pub async fn locate(
 81        starting_path: Option<LocateStart>,
 82        fs: Arc<dyn Fs>,
 83    ) -> anyhow::Result<PathBuf> {
 84        let paths_to_check = match starting_path.as_ref() {
 85            Some(starting_path) => {
 86                let worktree_root = starting_path
 87                    .worktree_root_path
 88                    .components()
 89                    .into_iter()
 90                    .take_while(|path_component| {
 91                        path_component.as_os_str().to_string_lossy() != "node_modules"
 92                    })
 93                    .collect::<PathBuf>();
 94
 95                if worktree_root != starting_path.worktree_root_path.as_ref() {
 96                    vec![worktree_root]
 97                } else {
 98                    let (worktree_root_metadata, start_path_metadata) = if starting_path
 99                        .starting_path
100                        .as_ref()
101                        == Path::new("")
102                    {
103                        let worktree_root_data =
104                            fs.metadata(&worktree_root).await.with_context(|| {
105                                format!(
106                                    "FS metadata fetch for worktree root path {worktree_root:?}",
107                                )
108                            })?;
109                        (worktree_root_data.unwrap_or_else(|| {
110                            panic!("cannot query prettier for non existing worktree root at {worktree_root_data:?}")
111                        }), None)
112                    } else {
113                        let full_starting_path = worktree_root.join(&starting_path.starting_path);
114                        let (worktree_root_data, start_path_data) = futures::try_join!(
115                            fs.metadata(&worktree_root),
116                            fs.metadata(&full_starting_path),
117                        )
118                        .with_context(|| {
119                            format!("FS metadata fetch for starting path {full_starting_path:?}",)
120                        })?;
121                        (
122                            worktree_root_data.unwrap_or_else(|| {
123                                panic!("cannot query prettier for non existing worktree root at {worktree_root_data:?}")
124                            }),
125                            start_path_data,
126                        )
127                    };
128
129                    match start_path_metadata {
130                        Some(start_path_metadata) => {
131                            anyhow::ensure!(worktree_root_metadata.is_dir,
132                                "For non-empty start path, worktree root {starting_path:?} should be a directory");
133                            anyhow::ensure!(
134                                !start_path_metadata.is_dir,
135                                "For non-empty start path, it should not be a directory {starting_path:?}"
136                            );
137                            anyhow::ensure!(
138                                !start_path_metadata.is_symlink,
139                                "For non-empty start path, it should not be a symlink {starting_path:?}"
140                            );
141
142                            let file_to_format = starting_path.starting_path.as_ref();
143                            let mut paths_to_check = VecDeque::from(vec![worktree_root.clone()]);
144                            let mut current_path = worktree_root;
145                            for path_component in file_to_format.components().into_iter() {
146                                current_path = current_path.join(path_component);
147                                paths_to_check.push_front(current_path.clone());
148                                if path_component.as_os_str().to_string_lossy() == "node_modules" {
149                                    break;
150                                }
151                            }
152                            paths_to_check.pop_front(); // last one is the file itself or node_modules, skip it
153                            Vec::from(paths_to_check)
154                        }
155                        None => {
156                            anyhow::ensure!(
157                                !worktree_root_metadata.is_dir,
158                                "For empty start path, worktree root should not be a directory {starting_path:?}"
159                            );
160                            anyhow::ensure!(
161                                !worktree_root_metadata.is_symlink,
162                                "For empty start path, worktree root should not be a symlink {starting_path:?}"
163                            );
164                            worktree_root
165                                .parent()
166                                .map(|path| vec![path.to_path_buf()])
167                                .unwrap_or_default()
168                        }
169                    }
170                }
171            }
172            None => Vec::new(),
173        };
174
175        match find_closest_prettier_dir(paths_to_check, fs.as_ref())
176            .await
177            .with_context(|| format!("finding prettier starting with {starting_path:?}"))?
178        {
179            Some(prettier_dir) => Ok(prettier_dir),
180            None => Ok(DEFAULT_PRETTIER_DIR.to_path_buf()),
181        }
182    }
183
184    pub async fn start(
185        worktree_id: Option<usize>,
186        server_id: LanguageServerId,
187        prettier_dir: PathBuf,
188        node: Arc<dyn NodeRuntime>,
189        cx: AsyncAppContext,
190    ) -> anyhow::Result<Self> {
191        let backgroud = cx.background();
192        anyhow::ensure!(
193            prettier_dir.is_dir(),
194            "Prettier dir {prettier_dir:?} is not a directory"
195        );
196        let prettier_server = DEFAULT_PRETTIER_DIR.join(PRETTIER_SERVER_FILE);
197        anyhow::ensure!(
198            prettier_server.is_file(),
199            "no prettier server package found at {prettier_server:?}"
200        );
201
202        let node_path = backgroud
203            .spawn(async move { node.binary_path().await })
204            .await?;
205        let server = LanguageServer::new(
206            server_id,
207            LanguageServerBinary {
208                path: node_path,
209                arguments: vec![prettier_server.into(), prettier_dir.as_path().into()],
210            },
211            Path::new("/"),
212            None,
213            cx,
214        )
215        .context("prettier server creation")?;
216        let server = backgroud
217            .spawn(server.initialize(None))
218            .await
219            .context("prettier server initialization")?;
220        Ok(Self::Local(Local {
221            worktree_id,
222            server,
223            default: prettier_dir == DEFAULT_PRETTIER_DIR.as_path(),
224            prettier_dir,
225        }))
226    }
227
228    pub async fn invoke(
229        &self,
230        buffer: Option<&ModelHandle<Buffer>>,
231        buffer_path: Option<PathBuf>,
232        method: &str,
233        cx: &AsyncAppContext,
234    ) -> anyhow::Result<Option<Diff>> {
235        match method {
236            Format::METHOD => self
237                .format(
238                    buffer.expect("missing buffer for format invocation"),
239                    buffer_path,
240                    cx,
241                )
242                .await
243                .context("invoke method")
244                .map(Some),
245            ClearCache::METHOD => {
246                self.clear_cache().await.context("invoke method")?;
247                Ok(None)
248            }
249            unknown => anyhow::bail!("Unknown method {unknown}"),
250        }
251    }
252
253    pub async fn format(
254        &self,
255        buffer: &ModelHandle<Buffer>,
256        buffer_path: Option<PathBuf>,
257        cx: &AsyncAppContext,
258    ) -> anyhow::Result<Diff> {
259        match self {
260            Self::Local(local) => {
261                let params = buffer.read_with(cx, |buffer, cx| {
262                    let buffer_language = buffer.language();
263                    let parsers_with_plugins = buffer_language
264                        .into_iter()
265                        .flat_map(|language| {
266                            language
267                                .lsp_adapters()
268                                .iter()
269                                .flat_map(|adapter| adapter.enabled_formatters())
270                                .filter_map(|formatter| match formatter {
271                                    BundledFormatter::Prettier {
272                                        parser_name,
273                                        plugin_names,
274                                    } => Some((parser_name, plugin_names)),
275                                })
276                        })
277                        .fold(
278                            HashMap::default(),
279                            |mut parsers_with_plugins, (parser_name, plugins)| {
280                                match parser_name {
281                                    Some(parser_name) => parsers_with_plugins
282                                        .entry(parser_name)
283                                        .or_insert_with(HashSet::default)
284                                        .extend(plugins),
285                                    None => parsers_with_plugins.values_mut().for_each(|existing_plugins| {
286                                        existing_plugins.extend(plugins.iter());
287                                    }),
288                                }
289                                parsers_with_plugins
290                            },
291                        );
292
293                    let selected_parser_with_plugins = parsers_with_plugins.iter().max_by_key(|(_, plugins)| plugins.len());
294                    if parsers_with_plugins.len() > 1 {
295                        log::warn!("Found multiple parsers with plugins {parsers_with_plugins:?}, will select only one: {selected_parser_with_plugins:?}");
296                    }
297
298                    let prettier_node_modules = self.prettier_dir().join("node_modules");
299                    anyhow::ensure!(prettier_node_modules.is_dir(), "Prettier node_modules dir does not exist: {prettier_node_modules:?}");
300                    let plugin_name_into_path = |plugin_name: &str| {
301                        let prettier_plugin_dir = prettier_node_modules.join(plugin_name);
302                        for possible_plugin_path in [
303                            prettier_plugin_dir.join("dist").join("index.mjs"),
304                            prettier_plugin_dir.join("index.mjs"),
305                            prettier_plugin_dir.join("plugin.js"),
306                            prettier_plugin_dir.join("index.js"),
307                            prettier_plugin_dir,
308                        ] {
309                            if possible_plugin_path.is_file() {
310                                return Some(possible_plugin_path);
311                            }
312                        }
313                        None
314                    };
315                    let (parser, located_plugins) = match selected_parser_with_plugins {
316                        Some((parser, plugins)) => {
317                            // Tailwind plugin requires being added last
318                            // https://github.com/tailwindlabs/prettier-plugin-tailwindcss#compatibility-with-other-prettier-plugins
319                            let mut add_tailwind_back = false;
320
321                            let mut plugins = plugins.into_iter().filter(|&&plugin_name| {
322                                if plugin_name == TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME {
323                                    add_tailwind_back = true;
324                                    false
325                                } else {
326                                    true
327                                }
328                            }).map(|plugin_name| (plugin_name, plugin_name_into_path(plugin_name))).collect::<Vec<_>>();
329                            if add_tailwind_back {
330                                plugins.push((&TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME, plugin_name_into_path(TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME)));
331                            }
332                            (Some(parser.to_string()), plugins)
333                        },
334                        None => (None, Vec::new()),
335                    };
336
337                    let prettier_options = if self.is_default() {
338                        let language_settings = language_settings(buffer_language, buffer.file(), cx);
339                        let mut options = language_settings.prettier.clone();
340                        if !options.contains_key("tabWidth") {
341                            options.insert(
342                                "tabWidth".to_string(),
343                                serde_json::Value::Number(serde_json::Number::from(
344                                    language_settings.tab_size.get(),
345                                )),
346                            );
347                        }
348                        if !options.contains_key("printWidth") {
349                            options.insert(
350                                "printWidth".to_string(),
351                                serde_json::Value::Number(serde_json::Number::from(
352                                    language_settings.preferred_line_length,
353                                )),
354                            );
355                        }
356                        Some(options)
357                    } else {
358                        None
359                    };
360
361                    let plugins = located_plugins.into_iter().filter_map(|(plugin_name, located_plugin_path)| {
362                        match located_plugin_path {
363                            Some(path) => Some(path),
364                            None => {
365                                log::error!("Have not found plugin path for {plugin_name:?} inside {prettier_node_modules:?}");
366                                None},
367                        }
368                    }).collect();
369                    log::debug!("Formatting file {:?} with prettier, plugins :{plugins:?}, options: {prettier_options:?}", buffer.file().map(|f| f.full_path(cx)));
370
371                    anyhow::Ok(FormatParams {
372                        text: buffer.text(),
373                        options: FormatOptions {
374                            parser,
375                            plugins,
376                            path: buffer_path,
377                            prettier_options,
378                        },
379                    })
380                }).context("prettier params calculation")?;
381                let response = local
382                    .server
383                    .request::<Format>(params)
384                    .await
385                    .context("prettier format request")?;
386                let diff_task = buffer.read_with(cx, |buffer, cx| buffer.diff(response.text, cx));
387                Ok(diff_task.await)
388            }
389            Self::Remote(remote) => buffer
390                .read_with(cx, |buffer, _| {
391                    remote.client.request(proto::InvokePrettierForBuffer {
392                        buffer_id: Some(buffer.remote_id()),
393                        worktree_id: self.worktree_id().map(|id| id as u64),
394                        method: Format::METHOD.to_string(),
395                        project_id: remote.project_id,
396                        prettier_path: remote.prettier_dir.to_string_lossy().to_string(),
397                    })
398                })
399                .await
400                .context("prettier diff invoke")?
401                .diff
402                .map(deserialize_diff)
403                .context("missing diff after prettier diff invocation"),
404        }
405    }
406
407    pub async fn clear_cache(&self) -> anyhow::Result<()> {
408        match self {
409            Self::Local(local) => local
410                .server
411                .request::<ClearCache>(())
412                .await
413                .context("prettier clear cache"),
414            Self::Remote(remote) => remote
415                .client
416                .request(proto::InvokePrettierForBuffer {
417                    buffer_id: None,
418                    worktree_id: self.worktree_id().map(|id| id as u64),
419                    method: ClearCache::METHOD.to_string(),
420                    project_id: remote.project_id,
421                    prettier_path: remote.prettier_dir.to_string_lossy().to_string(),
422                })
423                .await
424                .map(|response| {
425                    debug_assert!(
426                        response.diff.is_none(),
427                        "Cleare cache invocation returned diff data"
428                    )
429                })
430                .context("prettier invoke clear cache"),
431        }
432    }
433
434    pub fn server(&self) -> Option<&Arc<LanguageServer>> {
435        match self {
436            Prettier::Local(local) => Some(&local.server),
437            Prettier::Remote(_) => None,
438        }
439    }
440
441    pub fn is_default(&self) -> bool {
442        match self {
443            Prettier::Local(local) => local.default,
444            Prettier::Remote(_) => false,
445        }
446    }
447
448    pub fn prettier_dir(&self) -> &Path {
449        match self {
450            Prettier::Local(local) => &local.prettier_dir,
451            Prettier::Remote(remote) => &remote.prettier_dir,
452        }
453    }
454
455    pub fn worktree_id(&self) -> Option<usize> {
456        match self {
457            Prettier::Local(local) => local.worktree_id,
458            Prettier::Remote(remote) => remote.worktree_id,
459        }
460    }
461}
462
463async fn find_closest_prettier_dir(
464    paths_to_check: Vec<PathBuf>,
465    fs: &dyn Fs,
466) -> anyhow::Result<Option<PathBuf>> {
467    for path in paths_to_check {
468        let possible_package_json = path.join("package.json");
469        if let Some(package_json_metadata) = fs
470            .metadata(&possible_package_json)
471            .await
472            .with_context(|| format!("Fetching metadata for {possible_package_json:?}"))?
473        {
474            if !package_json_metadata.is_dir && !package_json_metadata.is_symlink {
475                let package_json_contents = fs
476                    .load(&possible_package_json)
477                    .await
478                    .with_context(|| format!("reading {possible_package_json:?} file contents"))?;
479                if let Ok(json_contents) = serde_json::from_str::<HashMap<String, serde_json::Value>>(
480                    &package_json_contents,
481                ) {
482                    if let Some(serde_json::Value::Object(o)) = json_contents.get("dependencies") {
483                        if o.contains_key(PRETTIER_PACKAGE_NAME) {
484                            return Ok(Some(path));
485                        }
486                    }
487                    if let Some(serde_json::Value::Object(o)) = json_contents.get("devDependencies")
488                    {
489                        if o.contains_key(PRETTIER_PACKAGE_NAME) {
490                            return Ok(Some(path));
491                        }
492                    }
493                }
494            }
495        }
496
497        let possible_node_modules_location = path.join("node_modules").join(PRETTIER_PACKAGE_NAME);
498        if let Some(node_modules_location_metadata) = fs
499            .metadata(&possible_node_modules_location)
500            .await
501            .with_context(|| format!("fetching metadata for {possible_node_modules_location:?}"))?
502        {
503            if node_modules_location_metadata.is_dir {
504                return Ok(Some(path));
505            }
506        }
507    }
508    Ok(None)
509}
510
511enum Format {}
512
513#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
514#[serde(rename_all = "camelCase")]
515struct FormatParams {
516    text: String,
517    options: FormatOptions,
518}
519
520#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
521#[serde(rename_all = "camelCase")]
522struct FormatOptions {
523    plugins: Vec<PathBuf>,
524    parser: Option<String>,
525    #[serde(rename = "filepath")]
526    path: Option<PathBuf>,
527    prettier_options: Option<HashMap<String, serde_json::Value>>,
528}
529
530#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
531#[serde(rename_all = "camelCase")]
532struct FormatResult {
533    text: String,
534}
535
536impl lsp::request::Request for Format {
537    type Params = FormatParams;
538    type Result = FormatResult;
539    const METHOD: &'static str = "prettier/format";
540}
541
542enum ClearCache {}
543
544impl lsp::request::Request for ClearCache {
545    type Params = ();
546    type Result = ();
547    const METHOD: &'static str = "prettier/clear_cache";
548}