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