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