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