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