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}