prettier.rs

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