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