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