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