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