1use anyhow::{anyhow, Context};
2use collections::{HashMap, HashSet};
3use fs::Fs;
4use gpui::{AsyncAppContext, Model};
5use language::{language_settings::language_settings, Buffer, Diff};
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(Clone)]
18pub enum Prettier {
19 Real(RealPrettier),
20 #[cfg(any(test, feature = "test-support"))]
21 Test(TestPrettier),
22}
23
24#[derive(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(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 "package.json",
58 "prettier.config.js",
59 "prettier.config.cjs",
60 ".editorconfig",
61 ];
62
63 pub async fn locate_prettier_installation(
64 fs: &dyn Fs,
65 installed_prettiers: &HashSet<PathBuf>,
66 locate_from: &Path,
67 ) -> anyhow::Result<ControlFlow<(), Option<PathBuf>>> {
68 let mut path_to_check = locate_from
69 .components()
70 .take_while(|component| component.as_os_str().to_string_lossy() != "node_modules")
71 .collect::<PathBuf>();
72 if path_to_check != locate_from {
73 log::debug!(
74 "Skipping prettier location for path {path_to_check:?} that is inside node_modules"
75 );
76 return Ok(ControlFlow::Break(()));
77 }
78 let path_to_check_metadata = fs
79 .metadata(&path_to_check)
80 .await
81 .with_context(|| format!("failed to get metadata for initial path {path_to_check:?}"))?
82 .with_context(|| format!("empty metadata for initial path {path_to_check:?}"))?;
83 if !path_to_check_metadata.is_dir {
84 path_to_check.pop();
85 }
86
87 let mut project_path_with_prettier_dependency = None;
88 loop {
89 if installed_prettiers.contains(&path_to_check) {
90 log::debug!("Found prettier path {path_to_check:?} in installed prettiers");
91 return Ok(ControlFlow::Continue(Some(path_to_check)));
92 } else if let Some(package_json_contents) =
93 read_package_json(fs, &path_to_check).await?
94 {
95 if has_prettier_in_package_json(&package_json_contents) {
96 if has_prettier_in_node_modules(fs, &path_to_check).await? {
97 log::debug!("Found prettier path {path_to_check:?} in both package.json and node_modules");
98 return Ok(ControlFlow::Continue(Some(path_to_check)));
99 } else if project_path_with_prettier_dependency.is_none() {
100 project_path_with_prettier_dependency = Some(path_to_check.clone());
101 }
102 } else {
103 match package_json_contents.get("workspaces") {
104 Some(serde_json::Value::Array(workspaces)) => {
105 match &project_path_with_prettier_dependency {
106 Some(project_path_with_prettier_dependency) => {
107 let subproject_path = project_path_with_prettier_dependency.strip_prefix(&path_to_check).expect("traversing path parents, should be able to strip prefix");
108 if workspaces.iter().filter_map(|value| {
109 if let serde_json::Value::String(s) = value {
110 Some(s.clone())
111 } else {
112 log::warn!("Skipping non-string 'workspaces' value: {value:?}");
113 None
114 }
115 }).any(|workspace_definition| {
116 if let Some(path_matcher) = PathMatcher::new(&[workspace_definition.clone()]).ok() {
117 path_matcher.is_match(subproject_path)
118 } else {
119 workspace_definition == subproject_path.to_string_lossy()
120 }
121 }) {
122 anyhow::ensure!(has_prettier_in_node_modules(fs, &path_to_check).await?, "Found prettier path {path_to_check:?} in the workspace root for project in {project_path_with_prettier_dependency:?}, but it's not installed into workspace root's node_modules");
123 log::info!("Found prettier path {path_to_check:?} in the workspace root for project in {project_path_with_prettier_dependency:?}");
124 return Ok(ControlFlow::Continue(Some(path_to_check)));
125 } else {
126 log::warn!("Skipping path {path_to_check:?} that has prettier in its 'node_modules' subdirectory, but is not included in its package.json workspaces {workspaces:?}");
127 }
128 }
129 None => {
130 log::warn!("Skipping path {path_to_check:?} that has prettier in its 'node_modules' subdirectory, but has no prettier in its package.json");
131 }
132 }
133 },
134 Some(unknown) => log::error!("Failed to parse workspaces for {path_to_check:?} from package.json, got {unknown:?}. Skipping."),
135 None => log::warn!("Skipping path {path_to_check:?} that has no prettier dependency and no workspaces section in its package.json"),
136 }
137 }
138 }
139
140 if !path_to_check.pop() {
141 match project_path_with_prettier_dependency {
142 Some(closest_prettier_discovered) => {
143 anyhow::bail!("No prettier found in node_modules for ancestors of {locate_from:?}, but discovered prettier package.json dependency in {closest_prettier_discovered:?}")
144 }
145 None => {
146 log::debug!("Found no prettier in ancestors of {locate_from:?}");
147 return Ok(ControlFlow::Continue(None));
148 }
149 }
150 }
151 }
152 }
153
154 #[cfg(any(test, feature = "test-support"))]
155 pub async fn start(
156 _: LanguageServerId,
157 prettier_dir: PathBuf,
158 _: Arc<dyn NodeRuntime>,
159 _: AsyncAppContext,
160 ) -> anyhow::Result<Self> {
161 Ok(Self::Test(TestPrettier {
162 default: prettier_dir == default_prettier_dir().as_path(),
163 prettier_dir,
164 }))
165 }
166
167 #[cfg(not(any(test, feature = "test-support")))]
168 pub async fn start(
169 server_id: LanguageServerId,
170 prettier_dir: PathBuf,
171 node: Arc<dyn NodeRuntime>,
172 cx: AsyncAppContext,
173 ) -> anyhow::Result<Self> {
174 use lsp::LanguageServerBinary;
175
176 let executor = cx.background_executor().clone();
177 anyhow::ensure!(
178 prettier_dir.is_dir(),
179 "Prettier dir {prettier_dir:?} is not a directory"
180 );
181 let prettier_server = default_prettier_dir().join(PRETTIER_SERVER_FILE);
182 anyhow::ensure!(
183 prettier_server.is_file(),
184 "no prettier server package found at {prettier_server:?}"
185 );
186
187 let node_path = executor
188 .spawn(async move { node.binary_path().await })
189 .await?;
190 let server = LanguageServer::new(
191 Arc::new(parking_lot::Mutex::new(None)),
192 server_id,
193 LanguageServerBinary {
194 path: node_path,
195 arguments: vec![prettier_server.into(), prettier_dir.as_path().into()],
196 env: None,
197 },
198 &prettier_dir,
199 None,
200 cx.clone(),
201 )
202 .context("prettier server creation")?;
203 let server = cx
204 .update(|cx| executor.spawn(server.initialize(None, cx)))?
205 .await
206 .context("prettier server initialization")?;
207 Ok(Self::Real(RealPrettier {
208 server,
209 default: prettier_dir == default_prettier_dir().as_path(),
210 prettier_dir,
211 }))
212 }
213
214 pub async fn format(
215 &self,
216 buffer: &Model<Buffer>,
217 buffer_path: Option<PathBuf>,
218 cx: &mut AsyncAppContext,
219 ) -> anyhow::Result<Diff> {
220 match self {
221 Self::Real(local) => {
222 let params = buffer
223 .update(cx, |buffer, cx| {
224 let buffer_language = buffer.language();
225 let language_settings = language_settings(buffer_language, buffer.file(), cx);
226 let prettier_settings = &language_settings.prettier;
227 anyhow::ensure!(
228 prettier_settings.allowed,
229 "Cannot format: prettier is not allowed for language {buffer_language:?}"
230 );
231 let prettier_node_modules = self.prettier_dir().join("node_modules");
232 anyhow::ensure!(
233 prettier_node_modules.is_dir(),
234 "Prettier node_modules dir does not exist: {prettier_node_modules:?}"
235 );
236 let plugin_name_into_path = |plugin_name: &str| {
237 let prettier_plugin_dir = prettier_node_modules.join(plugin_name);
238 [
239 prettier_plugin_dir.join("dist").join("index.mjs"),
240 prettier_plugin_dir.join("dist").join("index.js"),
241 prettier_plugin_dir.join("dist").join("plugin.js"),
242 prettier_plugin_dir.join("index.mjs"),
243 prettier_plugin_dir.join("index.js"),
244 prettier_plugin_dir.join("plugin.js"),
245 // this one is for @prettier/plugin-php
246 prettier_plugin_dir.join("standalone.js"),
247 prettier_plugin_dir,
248 ]
249 .into_iter()
250 .find(|possible_plugin_path| possible_plugin_path.is_file())
251 };
252
253 // Tailwind plugin requires being added last
254 // https://github.com/tailwindlabs/prettier-plugin-tailwindcss#compatibility-with-other-prettier-plugins
255 let mut add_tailwind_back = false;
256
257 let mut located_plugins = prettier_settings.plugins.iter()
258 .filter(|plugin_name| {
259 if plugin_name.as_str() == TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME {
260 add_tailwind_back = true;
261 false
262 } else {
263 true
264 }
265 })
266 .map(|plugin_name| {
267 let plugin_path = plugin_name_into_path(plugin_name);
268 (plugin_name.clone(), plugin_path)
269 })
270 .collect::<Vec<_>>();
271 if add_tailwind_back {
272 located_plugins.push((
273 TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME.to_owned(),
274 plugin_name_into_path(TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME),
275 ));
276 }
277
278 let prettier_options = if self.is_default() {
279 let mut options = prettier_settings.options.clone();
280 if !options.contains_key("tabWidth") {
281 options.insert(
282 "tabWidth".to_string(),
283 serde_json::Value::Number(serde_json::Number::from(
284 language_settings.tab_size.get(),
285 )),
286 );
287 }
288 if !options.contains_key("printWidth") {
289 options.insert(
290 "printWidth".to_string(),
291 serde_json::Value::Number(serde_json::Number::from(
292 language_settings.preferred_line_length,
293 )),
294 );
295 }
296 if !options.contains_key("useTabs") {
297 options.insert(
298 "useTabs".to_string(),
299 serde_json::Value::Bool(language_settings.hard_tabs),
300 );
301 }
302 Some(options)
303 } else {
304 None
305 };
306
307 let plugins = located_plugins
308 .into_iter()
309 .filter_map(|(plugin_name, located_plugin_path)| {
310 match located_plugin_path {
311 Some(path) => Some(path),
312 None => {
313 log::error!("Have not found plugin path for {plugin_name:?} inside {prettier_node_modules:?}");
314 None
315 }
316 }
317 })
318 .collect();
319
320 let mut prettier_parser = prettier_settings.parser.as_deref();
321 if buffer_path.is_none() {
322 prettier_parser = prettier_parser.or_else(|| buffer_language.and_then(|language| language.prettier_parser_name()));
323 if prettier_parser.is_none() {
324 log::error!("Formatting unsaved file with prettier failed. No prettier parser configured for language {buffer_language:?}");
325 return Err(anyhow!("Cannot determine prettier parser for unsaved file"));
326 }
327
328 }
329
330 log::debug!(
331 "Formatting file {:?} with prettier, plugins :{:?}, options: {:?}",
332 buffer.file().map(|f| f.full_path(cx)),
333 plugins,
334 prettier_options,
335 );
336
337 anyhow::Ok(FormatParams {
338 text: buffer.text(),
339 options: FormatOptions {
340 parser: prettier_parser.map(ToOwned::to_owned),
341 plugins,
342 path: buffer_path,
343 prettier_options,
344 },
345 })
346 })?
347 .context("prettier params calculation")?;
348
349 let response = local
350 .server
351 .request::<Format>(params)
352 .await
353 .context("prettier format request")?;
354 let diff_task = buffer.update(cx, |buffer, cx| buffer.diff(response.text, cx))?;
355 Ok(diff_task.await)
356 }
357 #[cfg(any(test, feature = "test-support"))]
358 Self::Test(_) => Ok(buffer
359 .update(cx, |buffer, cx| {
360 match buffer
361 .language()
362 .map(|language| language.lsp_id())
363 .as_deref()
364 {
365 Some("rust") => anyhow::bail!("prettier does not support Rust"),
366 Some(_other) => {
367 let formatted_text = buffer.text() + FORMAT_SUFFIX;
368 Ok(buffer.diff(formatted_text, cx))
369 }
370 None => panic!("Should not format buffer without a language with prettier"),
371 }
372 })??
373 .await),
374 }
375 }
376
377 pub async fn clear_cache(&self) -> anyhow::Result<()> {
378 match self {
379 Self::Real(local) => local
380 .server
381 .request::<ClearCache>(())
382 .await
383 .context("prettier clear cache"),
384 #[cfg(any(test, feature = "test-support"))]
385 Self::Test(_) => Ok(()),
386 }
387 }
388
389 pub fn server(&self) -> Option<&Arc<LanguageServer>> {
390 match self {
391 Self::Real(local) => Some(&local.server),
392 #[cfg(any(test, feature = "test-support"))]
393 Self::Test(_) => None,
394 }
395 }
396
397 pub fn is_default(&self) -> bool {
398 match self {
399 Self::Real(local) => local.default,
400 #[cfg(any(test, feature = "test-support"))]
401 Self::Test(test_prettier) => test_prettier.default,
402 }
403 }
404
405 pub fn prettier_dir(&self) -> &Path {
406 match self {
407 Self::Real(local) => &local.prettier_dir,
408 #[cfg(any(test, feature = "test-support"))]
409 Self::Test(test_prettier) => &test_prettier.prettier_dir,
410 }
411 }
412}
413
414async fn has_prettier_in_node_modules(fs: &dyn Fs, path: &Path) -> anyhow::Result<bool> {
415 let possible_node_modules_location = path.join("node_modules").join(PRETTIER_PACKAGE_NAME);
416 if let Some(node_modules_location_metadata) = fs
417 .metadata(&possible_node_modules_location)
418 .await
419 .with_context(|| format!("fetching metadata for {possible_node_modules_location:?}"))?
420 {
421 return Ok(node_modules_location_metadata.is_dir);
422 }
423 Ok(false)
424}
425
426async fn read_package_json(
427 fs: &dyn Fs,
428 path: &Path,
429) -> anyhow::Result<Option<HashMap<String, serde_json::Value>>> {
430 let possible_package_json = path.join("package.json");
431 if let Some(package_json_metadata) = fs
432 .metadata(&possible_package_json)
433 .await
434 .with_context(|| format!("fetching metadata for package json {possible_package_json:?}"))?
435 {
436 if !package_json_metadata.is_dir && !package_json_metadata.is_symlink {
437 let package_json_contents = fs
438 .load(&possible_package_json)
439 .await
440 .with_context(|| format!("reading {possible_package_json:?} file contents"))?;
441 return serde_json::from_str::<HashMap<String, serde_json::Value>>(
442 &package_json_contents,
443 )
444 .map(Some)
445 .with_context(|| format!("parsing {possible_package_json:?} file contents"));
446 }
447 }
448 Ok(None)
449}
450
451fn has_prettier_in_package_json(
452 package_json_contents: &HashMap<String, serde_json::Value>,
453) -> bool {
454 if let Some(serde_json::Value::Object(o)) = package_json_contents.get("dependencies") {
455 if o.contains_key(PRETTIER_PACKAGE_NAME) {
456 return true;
457 }
458 }
459 if let Some(serde_json::Value::Object(o)) = package_json_contents.get("devDependencies") {
460 if o.contains_key(PRETTIER_PACKAGE_NAME) {
461 return true;
462 }
463 }
464 false
465}
466
467enum Format {}
468
469#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
470#[serde(rename_all = "camelCase")]
471struct FormatParams {
472 text: String,
473 options: FormatOptions,
474}
475
476#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
477#[serde(rename_all = "camelCase")]
478struct FormatOptions {
479 plugins: Vec<PathBuf>,
480 parser: Option<String>,
481 #[serde(rename = "filepath")]
482 path: Option<PathBuf>,
483 prettier_options: Option<HashMap<String, serde_json::Value>>,
484}
485
486#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
487#[serde(rename_all = "camelCase")]
488struct FormatResult {
489 text: String,
490}
491
492impl lsp::request::Request for Format {
493 type Params = FormatParams;
494 type Result = FormatResult;
495 const METHOD: &'static str = "prettier/format";
496}
497
498enum ClearCache {}
499
500impl lsp::request::Request for ClearCache {
501 type Params = ();
502 type Result = ();
503 const METHOD: &'static str = "prettier/clear_cache";
504}
505
506#[cfg(test)]
507mod tests {
508 use fs::FakeFs;
509 use serde_json::json;
510
511 use super::*;
512
513 #[gpui::test]
514 async fn test_prettier_lookup_finds_nothing(cx: &mut gpui::TestAppContext) {
515 let fs = FakeFs::new(cx.executor());
516 fs.insert_tree(
517 "/root",
518 json!({
519 ".config": {
520 "zed": {
521 "settings.json": r#"{ "formatter": "auto" }"#,
522 },
523 },
524 "work": {
525 "project": {
526 "src": {
527 "index.js": "// index.js file contents",
528 },
529 "node_modules": {
530 "expect": {
531 "build": {
532 "print.js": "// print.js file contents",
533 },
534 "package.json": r#"{
535 "devDependencies": {
536 "prettier": "2.5.1"
537 }
538 }"#,
539 },
540 "prettier": {
541 "index.js": "// Dummy prettier package file",
542 },
543 },
544 "package.json": r#"{}"#
545 },
546 }
547 }),
548 )
549 .await;
550
551 assert!(
552 matches!(
553 Prettier::locate_prettier_installation(
554 fs.as_ref(),
555 &HashSet::default(),
556 Path::new("/root/.config/zed/settings.json"),
557 )
558 .await,
559 Ok(ControlFlow::Continue(None))
560 ),
561 "Should successfully find no prettier for path hierarchy without it"
562 );
563 assert!(
564 matches!(
565 Prettier::locate_prettier_installation(
566 fs.as_ref(),
567 &HashSet::default(),
568 Path::new("/root/work/project/src/index.js")
569 )
570 .await,
571 Ok(ControlFlow::Continue(None))
572 ),
573 "Should successfully find no prettier for path hierarchy that has node_modules with prettier, but no package.json mentions of it"
574 );
575 assert!(
576 matches!(
577 Prettier::locate_prettier_installation(
578 fs.as_ref(),
579 &HashSet::default(),
580 Path::new("/root/work/project/node_modules/expect/build/print.js")
581 )
582 .await,
583 Ok(ControlFlow::Break(()))
584 ),
585 "Should not format files inside node_modules/"
586 );
587 }
588
589 #[gpui::test]
590 async fn test_prettier_lookup_in_simple_npm_projects(cx: &mut gpui::TestAppContext) {
591 let fs = FakeFs::new(cx.executor());
592 fs.insert_tree(
593 "/root",
594 json!({
595 "web_blog": {
596 "node_modules": {
597 "prettier": {
598 "index.js": "// Dummy prettier package file",
599 },
600 "expect": {
601 "build": {
602 "print.js": "// print.js file contents",
603 },
604 "package.json": r#"{
605 "devDependencies": {
606 "prettier": "2.5.1"
607 }
608 }"#,
609 },
610 },
611 "pages": {
612 "[slug].tsx": "// [slug].tsx file contents",
613 },
614 "package.json": r#"{
615 "devDependencies": {
616 "prettier": "2.3.0"
617 },
618 "prettier": {
619 "semi": false,
620 "printWidth": 80,
621 "htmlWhitespaceSensitivity": "strict",
622 "tabWidth": 4
623 }
624 }"#
625 }
626 }),
627 )
628 .await;
629
630 assert_eq!(
631 Prettier::locate_prettier_installation(
632 fs.as_ref(),
633 &HashSet::default(),
634 Path::new("/root/web_blog/pages/[slug].tsx")
635 )
636 .await
637 .unwrap(),
638 ControlFlow::Continue(Some(PathBuf::from("/root/web_blog"))),
639 "Should find a preinstalled prettier in the project root"
640 );
641 assert_eq!(
642 Prettier::locate_prettier_installation(
643 fs.as_ref(),
644 &HashSet::default(),
645 Path::new("/root/web_blog/node_modules/expect/build/print.js")
646 )
647 .await
648 .unwrap(),
649 ControlFlow::Break(()),
650 "Should not allow formatting node_modules/ contents"
651 );
652 }
653
654 #[gpui::test]
655 async fn test_prettier_lookup_for_not_installed(cx: &mut gpui::TestAppContext) {
656 let fs = FakeFs::new(cx.executor());
657 fs.insert_tree(
658 "/root",
659 json!({
660 "work": {
661 "web_blog": {
662 "node_modules": {
663 "expect": {
664 "build": {
665 "print.js": "// print.js file contents",
666 },
667 "package.json": r#"{
668 "devDependencies": {
669 "prettier": "2.5.1"
670 }
671 }"#,
672 },
673 },
674 "pages": {
675 "[slug].tsx": "// [slug].tsx file contents",
676 },
677 "package.json": r#"{
678 "devDependencies": {
679 "prettier": "2.3.0"
680 },
681 "prettier": {
682 "semi": false,
683 "printWidth": 80,
684 "htmlWhitespaceSensitivity": "strict",
685 "tabWidth": 4
686 }
687 }"#
688 }
689 }
690 }),
691 )
692 .await;
693
694 match Prettier::locate_prettier_installation(
695 fs.as_ref(),
696 &HashSet::default(),
697 Path::new("/root/work/web_blog/pages/[slug].tsx")
698 )
699 .await {
700 Ok(path) => panic!("Expected to fail for prettier in package.json but not in node_modules found, but got path {path:?}"),
701 Err(e) => {
702 let message = e.to_string();
703 assert!(message.contains("/root/work/web_blog"), "Error message should mention which project had prettier defined");
704 },
705 };
706
707 assert_eq!(
708 Prettier::locate_prettier_installation(
709 fs.as_ref(),
710 &HashSet::from_iter(
711 [PathBuf::from("/root"), PathBuf::from("/root/work")].into_iter()
712 ),
713 Path::new("/root/work/web_blog/pages/[slug].tsx")
714 )
715 .await
716 .unwrap(),
717 ControlFlow::Continue(Some(PathBuf::from("/root/work"))),
718 "Should return closest cached value found without path checks"
719 );
720
721 assert_eq!(
722 Prettier::locate_prettier_installation(
723 fs.as_ref(),
724 &HashSet::default(),
725 Path::new("/root/work/web_blog/node_modules/expect/build/print.js")
726 )
727 .await
728 .unwrap(),
729 ControlFlow::Break(()),
730 "Should not allow formatting files inside node_modules/"
731 );
732 assert_eq!(
733 Prettier::locate_prettier_installation(
734 fs.as_ref(),
735 &HashSet::from_iter(
736 [PathBuf::from("/root"), PathBuf::from("/root/work")].into_iter()
737 ),
738 Path::new("/root/work/web_blog/node_modules/expect/build/print.js")
739 )
740 .await
741 .unwrap(),
742 ControlFlow::Break(()),
743 "Should ignore cache lookup for files inside node_modules/"
744 );
745 }
746
747 #[gpui::test]
748 async fn test_prettier_lookup_in_npm_workspaces(cx: &mut gpui::TestAppContext) {
749 let fs = FakeFs::new(cx.executor());
750 fs.insert_tree(
751 "/root",
752 json!({
753 "work": {
754 "full-stack-foundations": {
755 "exercises": {
756 "03.loading": {
757 "01.problem.loader": {
758 "app": {
759 "routes": {
760 "users+": {
761 "$username_+": {
762 "notes.tsx": "// notes.tsx file contents",
763 },
764 },
765 },
766 },
767 "node_modules": {
768 "test.js": "// test.js contents",
769 },
770 "package.json": r#"{
771 "devDependencies": {
772 "prettier": "^3.0.3"
773 }
774 }"#
775 },
776 },
777 },
778 "package.json": r#"{
779 "workspaces": ["exercises/*/*", "examples/*"]
780 }"#,
781 "node_modules": {
782 "prettier": {
783 "index.js": "// Dummy prettier package file",
784 },
785 },
786 },
787 }
788 }),
789 )
790 .await;
791
792 assert_eq!(
793 Prettier::locate_prettier_installation(
794 fs.as_ref(),
795 &HashSet::default(),
796 Path::new("/root/work/full-stack-foundations/exercises/03.loading/01.problem.loader/app/routes/users+/$username_+/notes.tsx"),
797 ).await.unwrap(),
798 ControlFlow::Continue(Some(PathBuf::from("/root/work/full-stack-foundations"))),
799 "Should ascend to the multi-workspace root and find the prettier there",
800 );
801
802 assert_eq!(
803 Prettier::locate_prettier_installation(
804 fs.as_ref(),
805 &HashSet::default(),
806 Path::new("/root/work/full-stack-foundations/node_modules/prettier/index.js")
807 )
808 .await
809 .unwrap(),
810 ControlFlow::Break(()),
811 "Should not allow formatting files inside root node_modules/"
812 );
813 assert_eq!(
814 Prettier::locate_prettier_installation(
815 fs.as_ref(),
816 &HashSet::default(),
817 Path::new("/root/work/full-stack-foundations/exercises/03.loading/01.problem.loader/node_modules/test.js")
818 )
819 .await
820 .unwrap(),
821 ControlFlow::Break(()),
822 "Should not allow formatting files inside submodule's node_modules/"
823 );
824 }
825
826 #[gpui::test]
827 async fn test_prettier_lookup_in_npm_workspaces_for_not_installed(
828 cx: &mut gpui::TestAppContext,
829 ) {
830 let fs = FakeFs::new(cx.executor());
831 fs.insert_tree(
832 "/root",
833 json!({
834 "work": {
835 "full-stack-foundations": {
836 "exercises": {
837 "03.loading": {
838 "01.problem.loader": {
839 "app": {
840 "routes": {
841 "users+": {
842 "$username_+": {
843 "notes.tsx": "// notes.tsx file contents",
844 },
845 },
846 },
847 },
848 "node_modules": {},
849 "package.json": r#"{
850 "devDependencies": {
851 "prettier": "^3.0.3"
852 }
853 }"#
854 },
855 },
856 },
857 "package.json": r#"{
858 "workspaces": ["exercises/*/*", "examples/*"]
859 }"#,
860 },
861 }
862 }),
863 )
864 .await;
865
866 match Prettier::locate_prettier_installation(
867 fs.as_ref(),
868 &HashSet::default(),
869 Path::new("/root/work/full-stack-foundations/exercises/03.loading/01.problem.loader/app/routes/users+/$username_+/notes.tsx")
870 )
871 .await {
872 Ok(path) => panic!("Expected to fail for prettier in package.json but not in node_modules found, but got path {path:?}"),
873 Err(e) => {
874 let message = e.to_string();
875 assert!(message.contains("/root/work/full-stack-foundations/exercises/03.loading/01.problem.loader"), "Error message should mention which project had prettier defined");
876 assert!(message.contains("/root/work/full-stack-foundations"), "Error message should mention potential candidates without prettier node_modules contents");
877 },
878 };
879 }
880}