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