prettier.rs

   1use anyhow::{Context as _, anyhow};
   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                                return Err(anyhow!("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.server.request::<Format>(params).await?;
 456                let diff_task = buffer.update(cx, |buffer, cx| buffer.diff(response.text, cx))?;
 457                Ok(diff_task.await)
 458            }
 459            #[cfg(any(test, feature = "test-support"))]
 460            Self::Test(_) => Ok(buffer
 461                .update(cx, |buffer, cx| {
 462                    match buffer
 463                        .language()
 464                        .map(|language| language.lsp_id())
 465                        .as_deref()
 466                    {
 467                        Some("rust") => anyhow::bail!("prettier does not support Rust"),
 468                        Some(_other) => {
 469                            let formatted_text = buffer.text() + FORMAT_SUFFIX;
 470                            Ok(buffer.diff(formatted_text, cx))
 471                        }
 472                        None => panic!("Should not format buffer without a language with prettier"),
 473                    }
 474                })??
 475                .await),
 476        }
 477    }
 478
 479    pub async fn clear_cache(&self) -> anyhow::Result<()> {
 480        match self {
 481            Self::Real(local) => local
 482                .server
 483                .request::<ClearCache>(())
 484                .await
 485                .context("prettier clear cache"),
 486            #[cfg(any(test, feature = "test-support"))]
 487            Self::Test(_) => Ok(()),
 488        }
 489    }
 490
 491    pub fn server(&self) -> Option<&Arc<LanguageServer>> {
 492        match self {
 493            Self::Real(local) => Some(&local.server),
 494            #[cfg(any(test, feature = "test-support"))]
 495            Self::Test(_) => None,
 496        }
 497    }
 498
 499    pub fn is_default(&self) -> bool {
 500        match self {
 501            Self::Real(local) => local.default,
 502            #[cfg(any(test, feature = "test-support"))]
 503            Self::Test(test_prettier) => test_prettier.default,
 504        }
 505    }
 506
 507    pub fn prettier_dir(&self) -> &Path {
 508        match self {
 509            Self::Real(local) => &local.prettier_dir,
 510            #[cfg(any(test, feature = "test-support"))]
 511            Self::Test(test_prettier) => &test_prettier.prettier_dir,
 512        }
 513    }
 514}
 515
 516async fn has_prettier_in_node_modules(fs: &dyn Fs, path: &Path) -> anyhow::Result<bool> {
 517    let possible_node_modules_location = path.join("node_modules").join(PRETTIER_PACKAGE_NAME);
 518    if let Some(node_modules_location_metadata) = fs
 519        .metadata(&possible_node_modules_location)
 520        .await
 521        .with_context(|| format!("fetching metadata for {possible_node_modules_location:?}"))?
 522    {
 523        return Ok(node_modules_location_metadata.is_dir);
 524    }
 525    Ok(false)
 526}
 527
 528async fn read_package_json(
 529    fs: &dyn Fs,
 530    path: &Path,
 531) -> anyhow::Result<Option<HashMap<String, serde_json::Value>>> {
 532    let possible_package_json = path.join("package.json");
 533    if let Some(package_json_metadata) = fs
 534        .metadata(&possible_package_json)
 535        .await
 536        .with_context(|| format!("fetching metadata for package json {possible_package_json:?}"))?
 537    {
 538        if !package_json_metadata.is_dir && !package_json_metadata.is_symlink {
 539            let package_json_contents = fs
 540                .load(&possible_package_json)
 541                .await
 542                .with_context(|| format!("reading {possible_package_json:?} file contents"))?;
 543            return serde_json::from_str::<HashMap<String, serde_json::Value>>(
 544                &package_json_contents,
 545            )
 546            .map(Some)
 547            .with_context(|| format!("parsing {possible_package_json:?} file contents"));
 548        }
 549    }
 550    Ok(None)
 551}
 552
 553enum Format {}
 554
 555#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
 556#[serde(rename_all = "camelCase")]
 557struct FormatParams {
 558    text: String,
 559    options: FormatOptions,
 560}
 561
 562#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
 563#[serde(rename_all = "camelCase")]
 564struct FormatOptions {
 565    plugins: Vec<PathBuf>,
 566    parser: Option<String>,
 567    #[serde(rename = "filepath")]
 568    path: Option<PathBuf>,
 569    prettier_options: Option<HashMap<String, serde_json::Value>>,
 570    ignore_path: Option<PathBuf>,
 571}
 572
 573#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
 574#[serde(rename_all = "camelCase")]
 575struct FormatResult {
 576    text: String,
 577}
 578
 579impl lsp::request::Request for Format {
 580    type Params = FormatParams;
 581    type Result = FormatResult;
 582    const METHOD: &'static str = "prettier/format";
 583}
 584
 585enum ClearCache {}
 586
 587impl lsp::request::Request for ClearCache {
 588    type Params = ();
 589    type Result = ();
 590    const METHOD: &'static str = "prettier/clear_cache";
 591}
 592
 593#[cfg(test)]
 594mod tests {
 595    use fs::FakeFs;
 596    use serde_json::json;
 597
 598    use super::*;
 599
 600    #[gpui::test]
 601    async fn test_prettier_lookup_finds_nothing(cx: &mut gpui::TestAppContext) {
 602        let fs = FakeFs::new(cx.executor());
 603        fs.insert_tree(
 604            "/root",
 605            json!({
 606                ".config": {
 607                    "zed": {
 608                        "settings.json": r#"{ "formatter": "auto" }"#,
 609                    },
 610                },
 611                "work": {
 612                    "project": {
 613                        "src": {
 614                            "index.js": "// index.js file contents",
 615                        },
 616                        "node_modules": {
 617                            "expect": {
 618                                "build": {
 619                                    "print.js": "// print.js file contents",
 620                                },
 621                                "package.json": r#"{
 622                                    "devDependencies": {
 623                                        "prettier": "2.5.1"
 624                                    }
 625                                }"#,
 626                            },
 627                            "prettier": {
 628                                "index.js": "// Dummy prettier package file",
 629                            },
 630                        },
 631                        "package.json": r#"{}"#
 632                    },
 633                }
 634            }),
 635        )
 636        .await;
 637
 638        assert_eq!(
 639            Prettier::locate_prettier_installation(
 640                fs.as_ref(),
 641                &HashSet::default(),
 642                Path::new("/root/.config/zed/settings.json"),
 643            )
 644            .await
 645            .unwrap(),
 646            ControlFlow::Continue(None),
 647            "Should find no prettier for path hierarchy without it"
 648        );
 649        assert_eq!(
 650            Prettier::locate_prettier_installation(
 651                fs.as_ref(),
 652                &HashSet::default(),
 653                Path::new("/root/work/project/src/index.js")
 654            )
 655            .await
 656            .unwrap(),
 657            ControlFlow::Continue(Some(PathBuf::from("/root/work/project"))),
 658            "Should successfully find a prettier for path hierarchy that has node_modules with prettier, but no package.json mentions of it"
 659        );
 660        assert_eq!(
 661            Prettier::locate_prettier_installation(
 662                fs.as_ref(),
 663                &HashSet::default(),
 664                Path::new("/root/work/project/node_modules/expect/build/print.js")
 665            )
 666            .await
 667            .unwrap(),
 668            ControlFlow::Break(()),
 669            "Should not format files inside node_modules/"
 670        );
 671    }
 672
 673    #[gpui::test]
 674    async fn test_prettier_lookup_in_simple_npm_projects(cx: &mut gpui::TestAppContext) {
 675        let fs = FakeFs::new(cx.executor());
 676        fs.insert_tree(
 677            "/root",
 678            json!({
 679                "web_blog": {
 680                    "node_modules": {
 681                        "prettier": {
 682                            "index.js": "// Dummy prettier package file",
 683                        },
 684                        "expect": {
 685                            "build": {
 686                                "print.js": "// print.js file contents",
 687                            },
 688                            "package.json": r#"{
 689                                "devDependencies": {
 690                                    "prettier": "2.5.1"
 691                                }
 692                            }"#,
 693                        },
 694                    },
 695                    "pages": {
 696                        "[slug].tsx": "// [slug].tsx file contents",
 697                    },
 698                    "package.json": r#"{
 699                        "devDependencies": {
 700                            "prettier": "2.3.0"
 701                        },
 702                        "prettier": {
 703                            "semi": false,
 704                            "printWidth": 80,
 705                            "htmlWhitespaceSensitivity": "strict",
 706                            "tabWidth": 4
 707                        }
 708                    }"#
 709                }
 710            }),
 711        )
 712        .await;
 713
 714        assert_eq!(
 715            Prettier::locate_prettier_installation(
 716                fs.as_ref(),
 717                &HashSet::default(),
 718                Path::new("/root/web_blog/pages/[slug].tsx")
 719            )
 720            .await
 721            .unwrap(),
 722            ControlFlow::Continue(Some(PathBuf::from("/root/web_blog"))),
 723            "Should find a preinstalled prettier in the project root"
 724        );
 725        assert_eq!(
 726            Prettier::locate_prettier_installation(
 727                fs.as_ref(),
 728                &HashSet::default(),
 729                Path::new("/root/web_blog/node_modules/expect/build/print.js")
 730            )
 731            .await
 732            .unwrap(),
 733            ControlFlow::Break(()),
 734            "Should not allow formatting node_modules/ contents"
 735        );
 736    }
 737
 738    #[gpui::test]
 739    async fn test_prettier_lookup_for_not_installed(cx: &mut gpui::TestAppContext) {
 740        let fs = FakeFs::new(cx.executor());
 741        fs.insert_tree(
 742            "/root",
 743            json!({
 744                "work": {
 745                    "web_blog": {
 746                        "node_modules": {
 747                            "expect": {
 748                                "build": {
 749                                    "print.js": "// print.js file contents",
 750                                },
 751                                "package.json": r#"{
 752                                    "devDependencies": {
 753                                        "prettier": "2.5.1"
 754                                    }
 755                                }"#,
 756                            },
 757                        },
 758                        "pages": {
 759                            "[slug].tsx": "// [slug].tsx file contents",
 760                        },
 761                        "package.json": r#"{
 762                            "devDependencies": {
 763                                "prettier": "2.3.0"
 764                            },
 765                            "prettier": {
 766                                "semi": false,
 767                                "printWidth": 80,
 768                                "htmlWhitespaceSensitivity": "strict",
 769                                "tabWidth": 4
 770                            }
 771                        }"#
 772                    }
 773                }
 774            }),
 775        )
 776        .await;
 777
 778        assert_eq!(
 779            Prettier::locate_prettier_installation(
 780                fs.as_ref(),
 781                &HashSet::default(),
 782                Path::new("/root/work/web_blog/pages/[slug].tsx")
 783            )
 784            .await
 785            .unwrap(),
 786            ControlFlow::Continue(None),
 787            "Should find no prettier when node_modules don't have it"
 788        );
 789
 790        assert_eq!(
 791            Prettier::locate_prettier_installation(
 792                fs.as_ref(),
 793                &HashSet::from_iter(
 794                    [PathBuf::from("/root"), PathBuf::from("/root/work")].into_iter()
 795                ),
 796                Path::new("/root/work/web_blog/pages/[slug].tsx")
 797            )
 798            .await
 799            .unwrap(),
 800            ControlFlow::Continue(Some(PathBuf::from("/root/work"))),
 801            "Should return closest cached value found without path checks"
 802        );
 803
 804        assert_eq!(
 805            Prettier::locate_prettier_installation(
 806                fs.as_ref(),
 807                &HashSet::default(),
 808                Path::new("/root/work/web_blog/node_modules/expect/build/print.js")
 809            )
 810            .await
 811            .unwrap(),
 812            ControlFlow::Break(()),
 813            "Should not allow formatting files inside node_modules/"
 814        );
 815        assert_eq!(
 816            Prettier::locate_prettier_installation(
 817                fs.as_ref(),
 818                &HashSet::from_iter(
 819                    [PathBuf::from("/root"), PathBuf::from("/root/work")].into_iter()
 820                ),
 821                Path::new("/root/work/web_blog/node_modules/expect/build/print.js")
 822            )
 823            .await
 824            .unwrap(),
 825            ControlFlow::Break(()),
 826            "Should ignore cache lookup for files inside node_modules/"
 827        );
 828    }
 829
 830    #[gpui::test]
 831    async fn test_prettier_lookup_in_npm_workspaces(cx: &mut gpui::TestAppContext) {
 832        let fs = FakeFs::new(cx.executor());
 833        fs.insert_tree(
 834            "/root",
 835            json!({
 836                "work": {
 837                    "full-stack-foundations": {
 838                        "exercises": {
 839                            "03.loading": {
 840                                "01.problem.loader": {
 841                                    "app": {
 842                                        "routes": {
 843                                            "users+": {
 844                                                "$username_+": {
 845                                                    "notes.tsx": "// notes.tsx file contents",
 846                                                },
 847                                            },
 848                                        },
 849                                    },
 850                                    "node_modules": {
 851                                        "test.js": "// test.js contents",
 852                                    },
 853                                    "package.json": r#"{
 854                                        "devDependencies": {
 855                                            "prettier": "^3.0.3"
 856                                        }
 857                                    }"#
 858                                },
 859                            },
 860                        },
 861                        "package.json": r#"{
 862                            "workspaces": ["exercises/*/*", "examples/*"]
 863                        }"#,
 864                        "node_modules": {
 865                            "prettier": {
 866                                "index.js": "// Dummy prettier package file",
 867                            },
 868                        },
 869                    },
 870                }
 871            }),
 872        )
 873        .await;
 874
 875        assert_eq!(
 876            Prettier::locate_prettier_installation(
 877                fs.as_ref(),
 878                &HashSet::default(),
 879                Path::new("/root/work/full-stack-foundations/exercises/03.loading/01.problem.loader/app/routes/users+/$username_+/notes.tsx"),
 880            ).await.unwrap(),
 881            ControlFlow::Continue(Some(PathBuf::from("/root/work/full-stack-foundations"))),
 882            "Should ascend to the multi-workspace root and find the prettier there",
 883        );
 884
 885        assert_eq!(
 886            Prettier::locate_prettier_installation(
 887                fs.as_ref(),
 888                &HashSet::default(),
 889                Path::new("/root/work/full-stack-foundations/node_modules/prettier/index.js")
 890            )
 891            .await
 892            .unwrap(),
 893            ControlFlow::Break(()),
 894            "Should not allow formatting files inside root node_modules/"
 895        );
 896        assert_eq!(
 897            Prettier::locate_prettier_installation(
 898                fs.as_ref(),
 899                &HashSet::default(),
 900                Path::new("/root/work/full-stack-foundations/exercises/03.loading/01.problem.loader/node_modules/test.js")
 901            )
 902            .await
 903            .unwrap(),
 904            ControlFlow::Break(()),
 905            "Should not allow formatting files inside submodule's node_modules/"
 906        );
 907    }
 908
 909    #[gpui::test]
 910    async fn test_prettier_lookup_in_npm_workspaces_for_not_installed(
 911        cx: &mut gpui::TestAppContext,
 912    ) {
 913        let fs = FakeFs::new(cx.executor());
 914        fs.insert_tree(
 915            "/root",
 916            json!({
 917                "work": {
 918                    "full-stack-foundations": {
 919                        "exercises": {
 920                            "03.loading": {
 921                                "01.problem.loader": {
 922                                    "app": {
 923                                        "routes": {
 924                                            "users+": {
 925                                                "$username_+": {
 926                                                    "notes.tsx": "// notes.tsx file contents",
 927                                                },
 928                                            },
 929                                        },
 930                                    },
 931                                    "node_modules": {},
 932                                    "package.json": r#"{
 933                                        "devDependencies": {
 934                                            "prettier": "^3.0.3"
 935                                        }
 936                                    }"#
 937                                },
 938                            },
 939                        },
 940                        "package.json": r#"{
 941                            "workspaces": ["exercises/*/*", "examples/*"]
 942                        }"#,
 943                    },
 944                }
 945            }),
 946        )
 947        .await;
 948
 949        match Prettier::locate_prettier_installation(
 950            fs.as_ref(),
 951            &HashSet::default(),
 952            Path::new("/root/work/full-stack-foundations/exercises/03.loading/01.problem.loader/app/routes/users+/$username_+/notes.tsx")
 953        )
 954        .await {
 955            Ok(path) => panic!("Expected to fail for prettier in package.json but not in node_modules found, but got path {path:?}"),
 956            Err(e) => {
 957                let message = e.to_string().replace("\\\\", "/");
 958                assert!(message.contains("/root/work/full-stack-foundations/exercises/03.loading/01.problem.loader"), "Error message should mention which project had prettier defined");
 959                assert!(message.contains("/root/work/full-stack-foundations"), "Error message should mention potential candidates without prettier node_modules contents");
 960            },
 961        };
 962    }
 963
 964    #[gpui::test]
 965    async fn test_prettier_ignore_with_editor_prettier(cx: &mut gpui::TestAppContext) {
 966        let fs = FakeFs::new(cx.executor());
 967        fs.insert_tree(
 968            "/root",
 969            json!({
 970                "project": {
 971                    "src": {
 972                        "index.js": "// index.js file contents",
 973                        "ignored.js": "// this file should be ignored",
 974                    },
 975                    ".prettierignore": "ignored.js",
 976                    "package.json": r#"{
 977                        "name": "test-project"
 978                    }"#
 979                }
 980            }),
 981        )
 982        .await;
 983
 984        assert_eq!(
 985            Prettier::locate_prettier_ignore(
 986                fs.as_ref(),
 987                &HashSet::default(),
 988                Path::new("/root/project/src/index.js"),
 989            )
 990            .await
 991            .unwrap(),
 992            ControlFlow::Continue(Some(PathBuf::from("/root/project"))),
 993            "Should find prettierignore in project root"
 994        );
 995    }
 996
 997    #[gpui::test]
 998    async fn test_prettier_ignore_in_monorepo_with_only_child_ignore(
 999        cx: &mut gpui::TestAppContext,
1000    ) {
1001        let fs = FakeFs::new(cx.executor());
1002        fs.insert_tree(
1003            "/root",
1004            json!({
1005                "monorepo": {
1006                    "node_modules": {
1007                        "prettier": {
1008                            "index.js": "// Dummy prettier package file",
1009                        }
1010                    },
1011                    "packages": {
1012                        "web": {
1013                            "src": {
1014                                "index.js": "// index.js contents",
1015                                "ignored.js": "// this should be ignored",
1016                            },
1017                            ".prettierignore": "ignored.js",
1018                            "package.json": r#"{
1019                                "name": "web-package"
1020                            }"#
1021                        }
1022                    },
1023                    "package.json": r#"{
1024                        "workspaces": ["packages/*"],
1025                        "devDependencies": {
1026                            "prettier": "^2.0.0"
1027                        }
1028                    }"#
1029                }
1030            }),
1031        )
1032        .await;
1033
1034        assert_eq!(
1035            Prettier::locate_prettier_ignore(
1036                fs.as_ref(),
1037                &HashSet::default(),
1038                Path::new("/root/monorepo/packages/web/src/index.js"),
1039            )
1040            .await
1041            .unwrap(),
1042            ControlFlow::Continue(Some(PathBuf::from("/root/monorepo/packages/web"))),
1043            "Should find prettierignore in child package"
1044        );
1045    }
1046
1047    #[gpui::test]
1048    async fn test_prettier_ignore_in_monorepo_with_root_and_child_ignores(
1049        cx: &mut gpui::TestAppContext,
1050    ) {
1051        let fs = FakeFs::new(cx.executor());
1052        fs.insert_tree(
1053            "/root",
1054            json!({
1055                "monorepo": {
1056                    "node_modules": {
1057                        "prettier": {
1058                            "index.js": "// Dummy prettier package file",
1059                        }
1060                    },
1061                    ".prettierignore": "main.js",
1062                    "packages": {
1063                        "web": {
1064                            "src": {
1065                                "main.js": "// this should not be ignored",
1066                                "ignored.js": "// this should be ignored",
1067                            },
1068                            ".prettierignore": "ignored.js",
1069                            "package.json": r#"{
1070                                "name": "web-package"
1071                            }"#
1072                        }
1073                    },
1074                    "package.json": r#"{
1075                        "workspaces": ["packages/*"],
1076                        "devDependencies": {
1077                            "prettier": "^2.0.0"
1078                        }
1079                    }"#
1080                }
1081            }),
1082        )
1083        .await;
1084
1085        assert_eq!(
1086            Prettier::locate_prettier_ignore(
1087                fs.as_ref(),
1088                &HashSet::default(),
1089                Path::new("/root/monorepo/packages/web/src/main.js"),
1090            )
1091            .await
1092            .unwrap(),
1093            ControlFlow::Continue(Some(PathBuf::from("/root/monorepo/packages/web"))),
1094            "Should find child package prettierignore first"
1095        );
1096
1097        assert_eq!(
1098            Prettier::locate_prettier_ignore(
1099                fs.as_ref(),
1100                &HashSet::default(),
1101                Path::new("/root/monorepo/packages/web/src/ignored.js"),
1102            )
1103            .await
1104            .unwrap(),
1105            ControlFlow::Continue(Some(PathBuf::from("/root/monorepo/packages/web"))),
1106            "Should find child package prettierignore first"
1107        );
1108    }
1109}