prettier.rs

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