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