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