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