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