prettier.rs

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