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