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