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