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