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