prettier.rs

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