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