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