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