prettier.rs

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