prettier2.rs

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