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