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