prettier.rs

   1use anyhow::Context as _;
   2use collections::{HashMap, HashSet};
   3use fs::Fs;
   4use gpui::{AsyncApp, Entity};
   5use language::language_settings::{LanguageSettings, PrettierSettings};
   6use language::{Buffer, Diff, Language, OffsetUtf16};
   7use lsp::{LanguageServer, LanguageServerId};
   8use node_runtime::NodeRuntime;
   9use paths::default_prettier_dir;
  10use serde::{Deserialize, Serialize};
  11use std::{
  12    ops::{ControlFlow, Range},
  13    path::{Path, PathBuf},
  14    sync::Arc,
  15    time::Duration,
  16};
  17use util::{
  18    paths::{PathMatcher, PathStyle},
  19    rel_path::RelPath,
  20};
  21
  22#[derive(Debug, Clone)]
  23pub enum Prettier {
  24    Real(RealPrettier),
  25    #[cfg(any(test, feature = "test-support"))]
  26    Test(TestPrettier),
  27}
  28
  29#[derive(Debug, Clone)]
  30pub struct RealPrettier {
  31    default: bool,
  32    prettier_dir: PathBuf,
  33    server: Arc<LanguageServer>,
  34}
  35
  36#[cfg(any(test, feature = "test-support"))]
  37#[derive(Debug, Clone)]
  38pub struct TestPrettier {
  39    prettier_dir: PathBuf,
  40    default: bool,
  41}
  42
  43pub const FAIL_THRESHOLD: usize = 4;
  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
  49#[cfg(any(test, feature = "test-support"))]
  50pub const FORMAT_SUFFIX: &str = "\nformatted by test prettier";
  51#[cfg(any(test, feature = "test-support"))]
  52pub const RANGE_FORMAT_SUFFIX: &str = "\nrange formatted by test prettier";
  53
  54impl Prettier {
  55    pub const CONFIG_FILE_NAMES: &'static [&'static str] = &[
  56        ".prettierrc",
  57        ".prettierrc.json",
  58        ".prettierrc.json5",
  59        ".prettierrc.yaml",
  60        ".prettierrc.yml",
  61        ".prettierrc.toml",
  62        ".prettierrc.js",
  63        ".prettierrc.cjs",
  64        ".prettierrc.mjs",
  65        ".prettierrc.ts",
  66        ".prettierrc.cts",
  67        ".prettierrc.mts",
  68        "package.json",
  69        "prettier.config.js",
  70        "prettier.config.cjs",
  71        "prettier.config.mjs",
  72        "prettier.config.ts",
  73        "prettier.config.cts",
  74        "prettier.config.mts",
  75        ".editorconfig",
  76        ".prettierignore",
  77    ];
  78
  79    pub async fn locate_prettier_installation(
  80        fs: &dyn Fs,
  81        installed_prettiers: &HashSet<PathBuf>,
  82        locate_from: &Path,
  83    ) -> anyhow::Result<ControlFlow<(), Option<PathBuf>>> {
  84        let mut path_to_check = locate_from
  85            .components()
  86            .take_while(|component| component.as_os_str().to_string_lossy() != "node_modules")
  87            .collect::<PathBuf>();
  88        if path_to_check != locate_from {
  89            log::debug!(
  90                "Skipping prettier location for path {path_to_check:?} that is inside node_modules"
  91            );
  92            return Ok(ControlFlow::Break(()));
  93        }
  94        let path_to_check_metadata = fs
  95            .metadata(&path_to_check)
  96            .await
  97            .with_context(|| format!("failed to get metadata for initial path {path_to_check:?}"))?
  98            .with_context(|| format!("empty metadata for initial path {path_to_check:?}"))?;
  99        if !path_to_check_metadata.is_dir {
 100            path_to_check.pop();
 101        }
 102
 103        let mut closest_package_json_path = None;
 104        loop {
 105            if installed_prettiers.contains(&path_to_check) {
 106                log::debug!("Found prettier path {path_to_check:?} in installed prettiers");
 107                return Ok(ControlFlow::Continue(Some(path_to_check)));
 108            } else if let Some(package_json_contents) =
 109                read_package_json(fs, &path_to_check).await?
 110            {
 111                if has_prettier_in_node_modules(fs, &path_to_check).await? {
 112                    log::debug!("Found prettier path {path_to_check:?} in the node_modules");
 113                    return Ok(ControlFlow::Continue(Some(path_to_check)));
 114                } else {
 115                    match &closest_package_json_path {
 116                        None => closest_package_json_path = Some(path_to_check.clone()),
 117                        Some(closest_package_json_path) => {
 118                            match package_json_contents.get("workspaces") {
 119                                Some(serde_json::Value::Array(workspaces)) => {
 120                                    let subproject_path = closest_package_json_path.strip_prefix(&path_to_check).expect("traversing path parents, should be able to strip prefix");
 121                                    if workspaces.iter().filter_map(|value| {
 122                                        if let serde_json::Value::String(s) = value {
 123                                            Some(s.clone())
 124                                        } else {
 125                                            log::warn!("Skipping non-string 'workspaces' value: {value:?}");
 126                                            None
 127                                        }
 128                                    }).any(|workspace_definition| {
 129                                        workspace_definition == subproject_path.to_string_lossy() || PathMatcher::new(&[workspace_definition], PathStyle::local()).ok().is_some_and(
 130                                            |path_matcher| RelPath::new(subproject_path, PathStyle::local()).is_ok_and(|path|  path_matcher.is_match(path)))
 131                                    }) {
 132                                        anyhow::ensure!(has_prettier_in_node_modules(fs, &path_to_check).await?,
 133                                            "Path {path_to_check:?} is the workspace root for project in \
 134                                            {closest_package_json_path:?}, but it has no prettier installed"
 135                                        );
 136                                        log::info!(
 137                                            "Found prettier path {path_to_check:?} in the workspace \
 138                                            root for project in {closest_package_json_path:?}"
 139                                        );
 140                                        return Ok(ControlFlow::Continue(Some(path_to_check)));
 141                                    } else {
 142                                        log::warn!(
 143                                            "Skipping path {path_to_check:?} workspace root with \
 144                                            workspaces {workspaces:?} that have no prettier installed"
 145                                        );
 146                                    }
 147                                }
 148                                Some(unknown) => log::error!(
 149                                    "Failed to parse workspaces for {path_to_check:?} from package.json, \
 150                                    got {unknown:?}. Skipping."
 151                                ),
 152                                None => log::warn!(
 153                                    "Skipping path {path_to_check:?} that has no prettier \
 154                                    dependency and no workspaces section in its package.json"
 155                                ),
 156                            }
 157                        }
 158                    }
 159                }
 160            }
 161
 162            if !path_to_check.pop() {
 163                log::debug!("Found no prettier in ancestors of {locate_from:?}");
 164                return Ok(ControlFlow::Continue(None));
 165            }
 166        }
 167    }
 168
 169    pub async fn locate_prettier_ignore(
 170        fs: &dyn Fs,
 171        prettier_ignores: &HashSet<PathBuf>,
 172        locate_from: &Path,
 173    ) -> anyhow::Result<ControlFlow<(), Option<PathBuf>>> {
 174        let mut path_to_check = locate_from
 175            .components()
 176            .take_while(|component| component.as_os_str().to_string_lossy() != "node_modules")
 177            .collect::<PathBuf>();
 178        if path_to_check != locate_from {
 179            log::debug!(
 180                "Skipping prettier ignore location for path {path_to_check:?} that is inside node_modules"
 181            );
 182            return Ok(ControlFlow::Break(()));
 183        }
 184
 185        let path_to_check_metadata = fs
 186            .metadata(&path_to_check)
 187            .await
 188            .with_context(|| format!("failed to get metadata for initial path {path_to_check:?}"))?
 189            .with_context(|| format!("empty metadata for initial path {path_to_check:?}"))?;
 190        if !path_to_check_metadata.is_dir {
 191            path_to_check.pop();
 192        }
 193
 194        let mut closest_package_json_path = None;
 195        loop {
 196            if prettier_ignores.contains(&path_to_check) {
 197                log::debug!("Found prettier ignore at {path_to_check:?}");
 198                return Ok(ControlFlow::Continue(Some(path_to_check)));
 199            } else if let Some(package_json_contents) =
 200                read_package_json(fs, &path_to_check).await?
 201            {
 202                let ignore_path = path_to_check.join(".prettierignore");
 203                if let Some(metadata) = fs
 204                    .metadata(&ignore_path)
 205                    .await
 206                    .with_context(|| format!("fetching metadata for {ignore_path:?}"))?
 207                    && !metadata.is_dir
 208                    && !metadata.is_symlink
 209                {
 210                    log::info!("Found prettier ignore at {ignore_path:?}");
 211                    return Ok(ControlFlow::Continue(Some(path_to_check)));
 212                }
 213                match &closest_package_json_path {
 214                    None => closest_package_json_path = Some(path_to_check.clone()),
 215                    Some(closest_package_json_path) => {
 216                        if let Some(serde_json::Value::Array(workspaces)) =
 217                            package_json_contents.get("workspaces")
 218                        {
 219                            let subproject_path = closest_package_json_path
 220                                .strip_prefix(&path_to_check)
 221                                .expect("traversing path parents, should be able to strip prefix");
 222
 223                            if workspaces
 224                                .iter()
 225                                .filter_map(|value| {
 226                                    if let serde_json::Value::String(s) = value {
 227                                        Some(s.clone())
 228                                    } else {
 229                                        log::warn!(
 230                                            "Skipping non-string 'workspaces' value: {value:?}"
 231                                        );
 232                                        None
 233                                    }
 234                                })
 235                                .any(|workspace_definition| {
 236                                    workspace_definition == subproject_path.to_string_lossy()
 237                                        || PathMatcher::new(
 238                                            &[workspace_definition],
 239                                            PathStyle::local(),
 240                                        )
 241                                        .ok()
 242                                        .is_some_and(
 243                                            |path_matcher| {
 244                                                RelPath::new(subproject_path, PathStyle::local())
 245                                                    .is_ok_and(|rel_path| {
 246                                                        path_matcher.is_match(rel_path)
 247                                                    })
 248                                            },
 249                                        )
 250                                })
 251                            {
 252                                let workspace_ignore = path_to_check.join(".prettierignore");
 253                                if let Some(metadata) = fs.metadata(&workspace_ignore).await?
 254                                    && !metadata.is_dir
 255                                {
 256                                    log::info!(
 257                                        "Found prettier ignore at workspace root {workspace_ignore:?}"
 258                                    );
 259                                    return Ok(ControlFlow::Continue(Some(path_to_check)));
 260                                }
 261                            }
 262                        }
 263                    }
 264                }
 265            }
 266
 267            if !path_to_check.pop() {
 268                log::debug!("Found no prettier ignore in ancestors of {locate_from:?}");
 269                return Ok(ControlFlow::Continue(None));
 270            }
 271        }
 272    }
 273
 274    #[cfg(any(test, feature = "test-support"))]
 275    pub async fn start(
 276        _: LanguageServerId,
 277        prettier_dir: PathBuf,
 278        _: NodeRuntime,
 279        _: Duration,
 280        _: AsyncApp,
 281    ) -> anyhow::Result<Self> {
 282        Ok(Self::Test(TestPrettier {
 283            default: prettier_dir == default_prettier_dir().as_path(),
 284            prettier_dir,
 285        }))
 286    }
 287
 288    #[cfg(not(any(test, feature = "test-support")))]
 289    pub async fn start(
 290        server_id: LanguageServerId,
 291        prettier_dir: PathBuf,
 292        node: NodeRuntime,
 293        request_timeout: Duration,
 294        mut cx: AsyncApp,
 295    ) -> anyhow::Result<Self> {
 296        use lsp::{LanguageServerBinary, LanguageServerName};
 297
 298        let executor = cx.background_executor().clone();
 299        anyhow::ensure!(
 300            prettier_dir.is_dir(),
 301            "Prettier dir {prettier_dir:?} is not a directory"
 302        );
 303        let prettier_server = default_prettier_dir().join(PRETTIER_SERVER_FILE);
 304        anyhow::ensure!(
 305            prettier_server.is_file(),
 306            "no prettier server package found at {prettier_server:?}"
 307        );
 308
 309        let node_path = executor
 310            .spawn(async move { node.binary_path().await })
 311            .await?;
 312        let server_name = LanguageServerName("prettier".into());
 313        let server_binary = LanguageServerBinary {
 314            path: node_path,
 315            arguments: vec![prettier_server.into(), prettier_dir.as_path().into()],
 316            env: None,
 317        };
 318
 319        let server = LanguageServer::new(
 320            Arc::new(parking_lot::Mutex::new(None)),
 321            server_id,
 322            server_name,
 323            server_binary,
 324            &prettier_dir,
 325            None,
 326            Default::default(),
 327            &mut cx,
 328        )
 329        .context("prettier server creation")?;
 330
 331        let server = cx
 332            .update(|cx| {
 333                let params = server.default_initialize_params(false, false, cx);
 334                let configuration = lsp::DidChangeConfigurationParams {
 335                    settings: Default::default(),
 336                };
 337                executor.spawn(server.initialize(params, configuration.into(), request_timeout, cx))
 338            })
 339            .await
 340            .context("prettier server initialization")?;
 341        Ok(Self::Real(RealPrettier {
 342            server,
 343            default: prettier_dir == default_prettier_dir().as_path(),
 344            prettier_dir,
 345        }))
 346    }
 347
 348    pub async fn format(
 349        &self,
 350        buffer: &Entity<Buffer>,
 351        buffer_path: Option<PathBuf>,
 352        ignore_dir: Option<PathBuf>,
 353        range_utf16: Option<Range<OffsetUtf16>>,
 354        request_timeout: Duration,
 355        cx: &mut AsyncApp,
 356    ) -> anyhow::Result<Diff> {
 357        match self {
 358            Self::Real(local) => {
 359                let params = buffer
 360                    .update(cx, |buffer, cx| {
 361                        let buffer_language = buffer.language().map(|language| language.as_ref());
 362                        let language_settings = LanguageSettings::for_buffer(&buffer, cx);
 363                        let prettier_settings = &language_settings.prettier;
 364                        anyhow::ensure!(
 365                            prettier_settings.allowed,
 366                            "Cannot format: prettier is not allowed for language {buffer_language:?}"
 367                        );
 368                        let prettier_node_modules = self.prettier_dir().join("node_modules");
 369                        anyhow::ensure!(
 370                            prettier_node_modules.is_dir(),
 371                            "Prettier node_modules dir does not exist: {prettier_node_modules:?}"
 372                        );
 373                        let plugin_name_into_path = |plugin_name: &str| {
 374                            let prettier_plugin_dir = prettier_node_modules.join(plugin_name);
 375                            [
 376                                prettier_plugin_dir.join("dist").join("index.mjs"),
 377                                prettier_plugin_dir.join("dist").join("index.js"),
 378                                prettier_plugin_dir.join("dist").join("plugin.js"),
 379                                prettier_plugin_dir.join("src").join("plugin.js"),
 380                                prettier_plugin_dir.join("lib").join("index.js"),
 381                                prettier_plugin_dir.join("index.mjs"),
 382                                prettier_plugin_dir.join("index.js"),
 383                                prettier_plugin_dir.join("plugin.js"),
 384                                // this one is for @prettier/plugin-php
 385                                prettier_plugin_dir.join("standalone.js"),
 386                                // this one is for prettier-plugin-latex
 387                                prettier_plugin_dir.join("dist").join("prettier-plugin-latex.js"),
 388                                prettier_plugin_dir,
 389                            ]
 390                            .into_iter()
 391                            .find(|possible_plugin_path| possible_plugin_path.is_file())
 392                        };
 393
 394                        // Tailwind plugin requires being added last
 395                        // https://github.com/tailwindlabs/prettier-plugin-tailwindcss#compatibility-with-other-prettier-plugins
 396                        let mut add_tailwind_back = false;
 397
 398                        let mut located_plugins = prettier_settings.plugins.iter()
 399                            .filter(|plugin_name| {
 400                                if plugin_name.as_str() == TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME {
 401                                    add_tailwind_back = true;
 402                                    false
 403                                } else {
 404                                    true
 405                                }
 406                            })
 407                            .map(|plugin_name| {
 408                                let plugin_path = plugin_name_into_path(plugin_name);
 409                                (plugin_name.clone(), plugin_path)
 410                            })
 411                            .collect::<Vec<_>>();
 412                        if add_tailwind_back {
 413                            located_plugins.push((
 414                                TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME.to_owned(),
 415                                plugin_name_into_path(TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME),
 416                            ));
 417                        }
 418
 419                        let prettier_options = if self.is_default() {
 420                            let mut options = prettier_settings.options.clone();
 421                            if !options.contains_key("tabWidth") {
 422                                options.insert(
 423                                    "tabWidth".to_string(),
 424                                    serde_json::Value::Number(serde_json::Number::from(
 425                                        language_settings.tab_size.get(),
 426                                    )),
 427                                );
 428                            }
 429                            if !options.contains_key("printWidth") {
 430                                options.insert(
 431                                    "printWidth".to_string(),
 432                                    serde_json::Value::Number(serde_json::Number::from(
 433                                        language_settings.preferred_line_length,
 434                                    )),
 435                                );
 436                            }
 437                            if !options.contains_key("useTabs") {
 438                                options.insert(
 439                                    "useTabs".to_string(),
 440                                    serde_json::Value::Bool(language_settings.hard_tabs),
 441                                );
 442                            }
 443                            Some(options)
 444                        } else {
 445                            None
 446                        };
 447
 448                        let plugins = located_plugins
 449                            .into_iter()
 450                            .filter_map(|(plugin_name, located_plugin_path)| {
 451                                match located_plugin_path {
 452                                    Some(path) => Some(path),
 453                                    None => {
 454                                        log::error!("Have not found plugin path for {plugin_name:?} inside {prettier_node_modules:?}");
 455                                        None
 456                                    }
 457                                }
 458                            })
 459                            .collect();
 460
 461                        let parser = prettier_parser_name(buffer_path.as_deref(), buffer_language, prettier_settings).context("getting prettier parser")?;
 462
 463                        let ignore_path = ignore_dir.and_then(|dir| {
 464                            let ignore_file = dir.join(".prettierignore");
 465                            ignore_file.is_file().then_some(ignore_file)
 466                        });
 467
 468                        log::debug!(
 469                            "Formatting file {:?} with prettier, plugins :{:?}, options: {:?}, ignore_path: {:?}",
 470                            buffer.file().map(|f| f.full_path(cx)),
 471                            plugins,
 472                            prettier_options,
 473                            ignore_path,
 474                        );
 475
 476                        anyhow::Ok(FormatParams {
 477                            text: buffer.text(),
 478                            options: FormatOptions {
 479                                path: buffer_path,
 480                                parser,
 481                                plugins,
 482                                prettier_options,
 483                                ignore_path,
 484                                range_start: range_utf16.as_ref().map(|r| r.start.0),
 485                                range_end: range_utf16.as_ref().map(|r| r.end.0),
 486                            },
 487                        })
 488                })
 489                .context("building prettier request")?;
 490
 491                let response = local
 492                    .server
 493                    .request::<Format>(params, request_timeout)
 494                    .await
 495                    .into_response()?;
 496                let diff_task = buffer.update(cx, |buffer, cx| buffer.diff(response.text, cx));
 497                Ok(diff_task.await)
 498            }
 499            #[cfg(any(test, feature = "test-support"))]
 500            Self::Test(_) => Ok(buffer
 501                .update(cx, |buffer, cx| {
 502                    match buffer
 503                        .language()
 504                        .map(|language| language.lsp_id())
 505                        .as_deref()
 506                    {
 507                        Some("rust") => anyhow::bail!("prettier does not support Rust"),
 508                        Some(_other) => {
 509                            let buffer_language =
 510                                buffer.language().map(|language| language.as_ref());
 511                            let language_settings = LanguageSettings::for_buffer(buffer, cx);
 512                            let prettier_settings = &language_settings.prettier;
 513                            let parser = prettier_parser_name(
 514                                buffer_path.as_deref(),
 515                                buffer_language,
 516                                prettier_settings,
 517                            )?;
 518
 519                            let formatted_text = if let Some(range) = &range_utf16 {
 520                                let text = buffer.text();
 521                                let start_byte = buffer.offset_utf16_to_offset(range.start);
 522                                let insert_at = text[start_byte..]
 523                                    .find('\n')
 524                                    .map(|pos| start_byte + pos)
 525                                    .unwrap_or(text.len());
 526                                let mut suffix = RANGE_FORMAT_SUFFIX.to_string();
 527                                if let Some(parser) = &parser {
 528                                    suffix = format!("{suffix}\n{parser}");
 529                                }
 530                                let mut result = String::new();
 531                                result.push_str(&text[..insert_at]);
 532                                result.push_str(&suffix);
 533                                result.push_str(&text[insert_at..]);
 534                                result
 535                            } else {
 536                                let mut text = buffer.text() + FORMAT_SUFFIX;
 537                                if let Some(parser) = &parser {
 538                                    text = format!("{text}\n{parser}");
 539                                }
 540                                text
 541                            };
 542
 543                            Ok(buffer.diff(formatted_text, cx))
 544                        }
 545                        None => panic!("Should not format buffer without a language with prettier"),
 546                    }
 547                })?
 548                .await),
 549        }
 550    }
 551
 552    pub async fn clear_cache(&self, request_timeout: Duration) -> anyhow::Result<()> {
 553        match self {
 554            Self::Real(local) => local
 555                .server
 556                .request::<ClearCache>((), request_timeout)
 557                .await
 558                .into_response()
 559                .context("prettier clear cache"),
 560            #[cfg(any(test, feature = "test-support"))]
 561            Self::Test(_) => Ok(()),
 562        }
 563    }
 564
 565    pub fn server(&self) -> Option<&Arc<LanguageServer>> {
 566        match self {
 567            Self::Real(local) => Some(&local.server),
 568            #[cfg(any(test, feature = "test-support"))]
 569            Self::Test(_) => None,
 570        }
 571    }
 572
 573    pub fn is_default(&self) -> bool {
 574        match self {
 575            Self::Real(local) => local.default,
 576            #[cfg(any(test, feature = "test-support"))]
 577            Self::Test(test_prettier) => test_prettier.default,
 578        }
 579    }
 580
 581    pub fn prettier_dir(&self) -> &Path {
 582        match self {
 583            Self::Real(local) => &local.prettier_dir,
 584            #[cfg(any(test, feature = "test-support"))]
 585            Self::Test(test_prettier) => &test_prettier.prettier_dir,
 586        }
 587    }
 588}
 589
 590fn prettier_parser_name(
 591    buffer_path: Option<&Path>,
 592    buffer_language: Option<&Language>,
 593    prettier_settings: &PrettierSettings,
 594) -> anyhow::Result<Option<String>> {
 595    let parser = if buffer_path.is_none() {
 596        let parser = prettier_settings
 597            .parser
 598            .as_deref()
 599            .or_else(|| buffer_language.and_then(|language| language.prettier_parser_name()));
 600        if parser.is_none() {
 601            log::error!(
 602                "Formatting unsaved file with prettier failed. No prettier parser configured for language {buffer_language:?}"
 603            );
 604            anyhow::bail!("Cannot determine prettier parser for unsaved file");
 605        }
 606        parser
 607    } else if let (Some(buffer_language), Some(buffer_path)) = (buffer_language, buffer_path)
 608        && buffer_path.extension().is_some_and(|extension| {
 609            !buffer_language
 610                .config()
 611                .matcher
 612                .path_suffixes
 613                .contains(&extension.to_string_lossy().into_owned())
 614        })
 615    {
 616        buffer_language.prettier_parser_name()
 617    } else {
 618        prettier_settings.parser.as_deref()
 619    };
 620
 621    Ok(parser.map(ToOwned::to_owned))
 622}
 623
 624async fn has_prettier_in_node_modules(fs: &dyn Fs, path: &Path) -> anyhow::Result<bool> {
 625    let possible_node_modules_location = path.join("node_modules").join(PRETTIER_PACKAGE_NAME);
 626    if let Some(node_modules_location_metadata) = fs
 627        .metadata(&possible_node_modules_location)
 628        .await
 629        .with_context(|| format!("fetching metadata for {possible_node_modules_location:?}"))?
 630    {
 631        return Ok(node_modules_location_metadata.is_dir);
 632    }
 633    Ok(false)
 634}
 635
 636async fn read_package_json(
 637    fs: &dyn Fs,
 638    path: &Path,
 639) -> anyhow::Result<Option<HashMap<String, serde_json::Value>>> {
 640    let possible_package_json = path.join("package.json");
 641    if let Some(package_json_metadata) = fs
 642        .metadata(&possible_package_json)
 643        .await
 644        .with_context(|| format!("fetching metadata for package json {possible_package_json:?}"))?
 645        && !package_json_metadata.is_dir
 646        && !package_json_metadata.is_symlink
 647    {
 648        let package_json_contents = fs
 649            .load(&possible_package_json)
 650            .await
 651            .with_context(|| format!("reading {possible_package_json:?} file contents"))?;
 652        return serde_json::from_str::<HashMap<String, serde_json::Value>>(&package_json_contents)
 653            .map(Some)
 654            .with_context(|| format!("parsing {possible_package_json:?} file contents"));
 655    }
 656    Ok(None)
 657}
 658
 659enum Format {}
 660
 661#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
 662#[serde(rename_all = "camelCase")]
 663struct FormatParams {
 664    text: String,
 665    options: FormatOptions,
 666}
 667
 668#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
 669#[serde(rename_all = "camelCase")]
 670struct FormatOptions {
 671    plugins: Vec<PathBuf>,
 672    parser: Option<String>,
 673    #[serde(rename = "filepath")]
 674    path: Option<PathBuf>,
 675    prettier_options: Option<HashMap<String, serde_json::Value>>,
 676    ignore_path: Option<PathBuf>,
 677    #[serde(skip_serializing_if = "Option::is_none")]
 678    range_start: Option<usize>,
 679    #[serde(skip_serializing_if = "Option::is_none")]
 680    range_end: Option<usize>,
 681}
 682
 683#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
 684#[serde(rename_all = "camelCase")]
 685struct FormatResult {
 686    text: String,
 687}
 688
 689impl lsp::request::Request for Format {
 690    type Params = FormatParams;
 691    type Result = FormatResult;
 692    const METHOD: &'static str = "prettier/format";
 693}
 694
 695enum ClearCache {}
 696
 697impl lsp::request::Request for ClearCache {
 698    type Params = ();
 699    type Result = ();
 700    const METHOD: &'static str = "prettier/clear_cache";
 701}
 702
 703#[cfg(test)]
 704mod tests {
 705    use fs::FakeFs;
 706    use serde_json::json;
 707
 708    use super::*;
 709
 710    #[gpui::test]
 711    async fn test_prettier_lookup_finds_nothing(cx: &mut gpui::TestAppContext) {
 712        let fs = FakeFs::new(cx.executor());
 713        fs.insert_tree(
 714            "/root",
 715            json!({
 716                ".config": {
 717                    "zed": {
 718                        "settings.json": r#"{ "formatter": "auto" }"#,
 719                    },
 720                },
 721                "work": {
 722                    "project": {
 723                        "src": {
 724                            "index.js": "// index.js file contents",
 725                        },
 726                        "node_modules": {
 727                            "expect": {
 728                                "build": {
 729                                    "print.js": "// print.js file contents",
 730                                },
 731                                "package.json": r#"{
 732                                    "devDependencies": {
 733                                        "prettier": "2.5.1"
 734                                    }
 735                                }"#,
 736                            },
 737                            "prettier": {
 738                                "index.js": "// Dummy prettier package file",
 739                            },
 740                        },
 741                        "package.json": r#"{}"#
 742                    },
 743                }
 744            }),
 745        )
 746        .await;
 747
 748        assert_eq!(
 749            Prettier::locate_prettier_installation(
 750                fs.as_ref(),
 751                &HashSet::default(),
 752                Path::new("/root/.config/zed/settings.json"),
 753            )
 754            .await
 755            .unwrap(),
 756            ControlFlow::Continue(None),
 757            "Should find no prettier for path hierarchy without it"
 758        );
 759        assert_eq!(
 760            Prettier::locate_prettier_installation(
 761                fs.as_ref(),
 762                &HashSet::default(),
 763                Path::new("/root/work/project/src/index.js")
 764            )
 765            .await
 766            .unwrap(),
 767            ControlFlow::Continue(Some(PathBuf::from("/root/work/project"))),
 768            "Should successfully find a prettier for path hierarchy that has node_modules with prettier, but no package.json mentions of it"
 769        );
 770        assert_eq!(
 771            Prettier::locate_prettier_installation(
 772                fs.as_ref(),
 773                &HashSet::default(),
 774                Path::new("/root/work/project/node_modules/expect/build/print.js")
 775            )
 776            .await
 777            .unwrap(),
 778            ControlFlow::Break(()),
 779            "Should not format files inside node_modules/"
 780        );
 781    }
 782
 783    #[gpui::test]
 784    async fn test_prettier_lookup_in_simple_npm_projects(cx: &mut gpui::TestAppContext) {
 785        let fs = FakeFs::new(cx.executor());
 786        fs.insert_tree(
 787            "/root",
 788            json!({
 789                "web_blog": {
 790                    "node_modules": {
 791                        "prettier": {
 792                            "index.js": "// Dummy prettier package file",
 793                        },
 794                        "expect": {
 795                            "build": {
 796                                "print.js": "// print.js file contents",
 797                            },
 798                            "package.json": r#"{
 799                                "devDependencies": {
 800                                    "prettier": "2.5.1"
 801                                }
 802                            }"#,
 803                        },
 804                    },
 805                    "pages": {
 806                        "[slug].tsx": "// [slug].tsx file contents",
 807                    },
 808                    "package.json": r#"{
 809                        "devDependencies": {
 810                            "prettier": "2.3.0"
 811                        },
 812                        "prettier": {
 813                            "semi": false,
 814                            "printWidth": 80,
 815                            "htmlWhitespaceSensitivity": "strict",
 816                            "tabWidth": 4
 817                        }
 818                    }"#
 819                }
 820            }),
 821        )
 822        .await;
 823
 824        assert_eq!(
 825            Prettier::locate_prettier_installation(
 826                fs.as_ref(),
 827                &HashSet::default(),
 828                Path::new("/root/web_blog/pages/[slug].tsx")
 829            )
 830            .await
 831            .unwrap(),
 832            ControlFlow::Continue(Some(PathBuf::from("/root/web_blog"))),
 833            "Should find a preinstalled prettier in the project root"
 834        );
 835        assert_eq!(
 836            Prettier::locate_prettier_installation(
 837                fs.as_ref(),
 838                &HashSet::default(),
 839                Path::new("/root/web_blog/node_modules/expect/build/print.js")
 840            )
 841            .await
 842            .unwrap(),
 843            ControlFlow::Break(()),
 844            "Should not allow formatting node_modules/ contents"
 845        );
 846    }
 847
 848    #[gpui::test]
 849    async fn test_prettier_lookup_for_not_installed(cx: &mut gpui::TestAppContext) {
 850        let fs = FakeFs::new(cx.executor());
 851        fs.insert_tree(
 852            "/root",
 853            json!({
 854                "work": {
 855                    "web_blog": {
 856                        "node_modules": {
 857                            "expect": {
 858                                "build": {
 859                                    "print.js": "// print.js file contents",
 860                                },
 861                                "package.json": r#"{
 862                                    "devDependencies": {
 863                                        "prettier": "2.5.1"
 864                                    }
 865                                }"#,
 866                            },
 867                        },
 868                        "pages": {
 869                            "[slug].tsx": "// [slug].tsx file contents",
 870                        },
 871                        "package.json": r#"{
 872                            "devDependencies": {
 873                                "prettier": "2.3.0"
 874                            },
 875                            "prettier": {
 876                                "semi": false,
 877                                "printWidth": 80,
 878                                "htmlWhitespaceSensitivity": "strict",
 879                                "tabWidth": 4
 880                            }
 881                        }"#
 882                    }
 883                }
 884            }),
 885        )
 886        .await;
 887
 888        assert_eq!(
 889            Prettier::locate_prettier_installation(
 890                fs.as_ref(),
 891                &HashSet::default(),
 892                Path::new("/root/work/web_blog/pages/[slug].tsx")
 893            )
 894            .await
 895            .unwrap(),
 896            ControlFlow::Continue(None),
 897            "Should find no prettier when node_modules don't have it"
 898        );
 899
 900        assert_eq!(
 901            Prettier::locate_prettier_installation(
 902                fs.as_ref(),
 903                &HashSet::from_iter(
 904                    [PathBuf::from("/root"), PathBuf::from("/root/work")].into_iter()
 905                ),
 906                Path::new("/root/work/web_blog/pages/[slug].tsx")
 907            )
 908            .await
 909            .unwrap(),
 910            ControlFlow::Continue(Some(PathBuf::from("/root/work"))),
 911            "Should return closest cached value found without path checks"
 912        );
 913
 914        assert_eq!(
 915            Prettier::locate_prettier_installation(
 916                fs.as_ref(),
 917                &HashSet::default(),
 918                Path::new("/root/work/web_blog/node_modules/expect/build/print.js")
 919            )
 920            .await
 921            .unwrap(),
 922            ControlFlow::Break(()),
 923            "Should not allow formatting files inside node_modules/"
 924        );
 925        assert_eq!(
 926            Prettier::locate_prettier_installation(
 927                fs.as_ref(),
 928                &HashSet::from_iter(
 929                    [PathBuf::from("/root"), PathBuf::from("/root/work")].into_iter()
 930                ),
 931                Path::new("/root/work/web_blog/node_modules/expect/build/print.js")
 932            )
 933            .await
 934            .unwrap(),
 935            ControlFlow::Break(()),
 936            "Should ignore cache lookup for files inside node_modules/"
 937        );
 938    }
 939
 940    #[gpui::test]
 941    async fn test_prettier_lookup_in_npm_workspaces(cx: &mut gpui::TestAppContext) {
 942        let fs = FakeFs::new(cx.executor());
 943        fs.insert_tree(
 944            "/root",
 945            json!({
 946                "work": {
 947                    "full-stack-foundations": {
 948                        "exercises": {
 949                            "03.loading": {
 950                                "01.problem.loader": {
 951                                    "app": {
 952                                        "routes": {
 953                                            "users+": {
 954                                                "$username_+": {
 955                                                    "notes.tsx": "// notes.tsx file contents",
 956                                                },
 957                                            },
 958                                        },
 959                                    },
 960                                    "node_modules": {
 961                                        "test.js": "// test.js contents",
 962                                    },
 963                                    "package.json": r#"{
 964                                        "devDependencies": {
 965                                            "prettier": "^3.0.3"
 966                                        }
 967                                    }"#
 968                                },
 969                            },
 970                        },
 971                        "package.json": r#"{
 972                            "workspaces": ["exercises/*/*", "examples/*"]
 973                        }"#,
 974                        "node_modules": {
 975                            "prettier": {
 976                                "index.js": "// Dummy prettier package file",
 977                            },
 978                        },
 979                    },
 980                }
 981            }),
 982        )
 983        .await;
 984
 985        assert_eq!(
 986            Prettier::locate_prettier_installation(
 987                fs.as_ref(),
 988                &HashSet::default(),
 989                Path::new("/root/work/full-stack-foundations/exercises/03.loading/01.problem.loader/app/routes/users+/$username_+/notes.tsx"),
 990            ).await.unwrap(),
 991            ControlFlow::Continue(Some(PathBuf::from("/root/work/full-stack-foundations"))),
 992            "Should ascend to the multi-workspace root and find the prettier there",
 993        );
 994
 995        assert_eq!(
 996            Prettier::locate_prettier_installation(
 997                fs.as_ref(),
 998                &HashSet::default(),
 999                Path::new("/root/work/full-stack-foundations/node_modules/prettier/index.js")
1000            )
1001            .await
1002            .unwrap(),
1003            ControlFlow::Break(()),
1004            "Should not allow formatting files inside root node_modules/"
1005        );
1006        assert_eq!(
1007            Prettier::locate_prettier_installation(
1008                fs.as_ref(),
1009                &HashSet::default(),
1010                Path::new("/root/work/full-stack-foundations/exercises/03.loading/01.problem.loader/node_modules/test.js")
1011            )
1012            .await
1013            .unwrap(),
1014            ControlFlow::Break(()),
1015            "Should not allow formatting files inside submodule's node_modules/"
1016        );
1017    }
1018
1019    #[gpui::test]
1020    async fn test_prettier_lookup_in_npm_workspaces_for_not_installed(
1021        cx: &mut gpui::TestAppContext,
1022    ) {
1023        let fs = FakeFs::new(cx.executor());
1024        fs.insert_tree(
1025            "/root",
1026            json!({
1027                "work": {
1028                    "full-stack-foundations": {
1029                        "exercises": {
1030                            "03.loading": {
1031                                "01.problem.loader": {
1032                                    "app": {
1033                                        "routes": {
1034                                            "users+": {
1035                                                "$username_+": {
1036                                                    "notes.tsx": "// notes.tsx file contents",
1037                                                },
1038                                            },
1039                                        },
1040                                    },
1041                                    "node_modules": {},
1042                                    "package.json": r#"{
1043                                        "devDependencies": {
1044                                            "prettier": "^3.0.3"
1045                                        }
1046                                    }"#
1047                                },
1048                            },
1049                        },
1050                        "package.json": r#"{
1051                            "workspaces": ["exercises/*/*", "examples/*"]
1052                        }"#,
1053                    },
1054                }
1055            }),
1056        )
1057        .await;
1058
1059        match Prettier::locate_prettier_installation(
1060            fs.as_ref(),
1061            &HashSet::default(),
1062            Path::new("/root/work/full-stack-foundations/exercises/03.loading/01.problem.loader/app/routes/users+/$username_+/notes.tsx")
1063        )
1064        .await {
1065            Ok(path) => panic!("Expected to fail for prettier in package.json but not in node_modules found, but got path {path:?}"),
1066            Err(e) => {
1067                let message = e.to_string().replace("\\\\", "/");
1068                assert!(message.contains("/root/work/full-stack-foundations/exercises/03.loading/01.problem.loader"), "Error message should mention which project had prettier defined");
1069                assert!(message.contains("/root/work/full-stack-foundations"), "Error message should mention potential candidates without prettier node_modules contents");
1070            },
1071        };
1072    }
1073
1074    #[gpui::test]
1075    async fn test_prettier_ignore_with_editor_prettier(cx: &mut gpui::TestAppContext) {
1076        let fs = FakeFs::new(cx.executor());
1077        fs.insert_tree(
1078            "/root",
1079            json!({
1080                "project": {
1081                    "src": {
1082                        "index.js": "// index.js file contents",
1083                        "ignored.js": "// this file should be ignored",
1084                    },
1085                    ".prettierignore": "ignored.js",
1086                    "package.json": r#"{
1087                        "name": "test-project"
1088                    }"#
1089                }
1090            }),
1091        )
1092        .await;
1093
1094        assert_eq!(
1095            Prettier::locate_prettier_ignore(
1096                fs.as_ref(),
1097                &HashSet::default(),
1098                Path::new("/root/project/src/index.js"),
1099            )
1100            .await
1101            .unwrap(),
1102            ControlFlow::Continue(Some(PathBuf::from("/root/project"))),
1103            "Should find prettierignore in project root"
1104        );
1105    }
1106
1107    #[gpui::test]
1108    async fn test_prettier_ignore_in_monorepo_with_only_child_ignore(
1109        cx: &mut gpui::TestAppContext,
1110    ) {
1111        let fs = FakeFs::new(cx.executor());
1112        fs.insert_tree(
1113            "/root",
1114            json!({
1115                "monorepo": {
1116                    "node_modules": {
1117                        "prettier": {
1118                            "index.js": "// Dummy prettier package file",
1119                        }
1120                    },
1121                    "packages": {
1122                        "web": {
1123                            "src": {
1124                                "index.js": "// index.js contents",
1125                                "ignored.js": "// this should be ignored",
1126                            },
1127                            ".prettierignore": "ignored.js",
1128                            "package.json": r#"{
1129                                "name": "web-package"
1130                            }"#
1131                        }
1132                    },
1133                    "package.json": r#"{
1134                        "workspaces": ["packages/*"],
1135                        "devDependencies": {
1136                            "prettier": "^2.0.0"
1137                        }
1138                    }"#
1139                }
1140            }),
1141        )
1142        .await;
1143
1144        assert_eq!(
1145            Prettier::locate_prettier_ignore(
1146                fs.as_ref(),
1147                &HashSet::default(),
1148                Path::new("/root/monorepo/packages/web/src/index.js"),
1149            )
1150            .await
1151            .unwrap(),
1152            ControlFlow::Continue(Some(PathBuf::from("/root/monorepo/packages/web"))),
1153            "Should find prettierignore in child package"
1154        );
1155    }
1156
1157    #[gpui::test]
1158    async fn test_prettier_ignore_in_monorepo_with_root_and_child_ignores(
1159        cx: &mut gpui::TestAppContext,
1160    ) {
1161        let fs = FakeFs::new(cx.executor());
1162        fs.insert_tree(
1163            "/root",
1164            json!({
1165                "monorepo": {
1166                    "node_modules": {
1167                        "prettier": {
1168                            "index.js": "// Dummy prettier package file",
1169                        }
1170                    },
1171                    ".prettierignore": "main.js",
1172                    "packages": {
1173                        "web": {
1174                            "src": {
1175                                "main.js": "// this should not be ignored",
1176                                "ignored.js": "// this should be ignored",
1177                            },
1178                            ".prettierignore": "ignored.js",
1179                            "package.json": r#"{
1180                                "name": "web-package"
1181                            }"#
1182                        }
1183                    },
1184                    "package.json": r#"{
1185                        "workspaces": ["packages/*"],
1186                        "devDependencies": {
1187                            "prettier": "^2.0.0"
1188                        }
1189                    }"#
1190                }
1191            }),
1192        )
1193        .await;
1194
1195        assert_eq!(
1196            Prettier::locate_prettier_ignore(
1197                fs.as_ref(),
1198                &HashSet::default(),
1199                Path::new("/root/monorepo/packages/web/src/main.js"),
1200            )
1201            .await
1202            .unwrap(),
1203            ControlFlow::Continue(Some(PathBuf::from("/root/monorepo/packages/web"))),
1204            "Should find child package prettierignore first"
1205        );
1206
1207        assert_eq!(
1208            Prettier::locate_prettier_ignore(
1209                fs.as_ref(),
1210                &HashSet::default(),
1211                Path::new("/root/monorepo/packages/web/src/ignored.js"),
1212            )
1213            .await
1214            .unwrap(),
1215            ControlFlow::Continue(Some(PathBuf::from("/root/monorepo/packages/web"))),
1216            "Should find child package prettierignore first"
1217        );
1218    }
1219}