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("building prettier request")?;
456
457 let response = local
458 .server
459 .request::<Format>(params)
460 .await
461 .into_response()?;
462 let diff_task = buffer.update(cx, |buffer, cx| buffer.diff(response.text, cx))?;
463 Ok(diff_task.await)
464 }
465 #[cfg(any(test, feature = "test-support"))]
466 Self::Test(_) => Ok(buffer
467 .update(cx, |buffer, cx| {
468 match buffer
469 .language()
470 .map(|language| language.lsp_id())
471 .as_deref()
472 {
473 Some("rust") => anyhow::bail!("prettier does not support Rust"),
474 Some(_other) => {
475 let formatted_text = buffer.text() + FORMAT_SUFFIX;
476 Ok(buffer.diff(formatted_text, cx))
477 }
478 None => panic!("Should not format buffer without a language with prettier"),
479 }
480 })??
481 .await),
482 }
483 }
484
485 pub async fn clear_cache(&self) -> anyhow::Result<()> {
486 match self {
487 Self::Real(local) => local
488 .server
489 .request::<ClearCache>(())
490 .await
491 .into_response()
492 .context("prettier clear cache"),
493 #[cfg(any(test, feature = "test-support"))]
494 Self::Test(_) => Ok(()),
495 }
496 }
497
498 pub fn server(&self) -> Option<&Arc<LanguageServer>> {
499 match self {
500 Self::Real(local) => Some(&local.server),
501 #[cfg(any(test, feature = "test-support"))]
502 Self::Test(_) => None,
503 }
504 }
505
506 pub fn is_default(&self) -> bool {
507 match self {
508 Self::Real(local) => local.default,
509 #[cfg(any(test, feature = "test-support"))]
510 Self::Test(test_prettier) => test_prettier.default,
511 }
512 }
513
514 pub fn prettier_dir(&self) -> &Path {
515 match self {
516 Self::Real(local) => &local.prettier_dir,
517 #[cfg(any(test, feature = "test-support"))]
518 Self::Test(test_prettier) => &test_prettier.prettier_dir,
519 }
520 }
521}
522
523async fn has_prettier_in_node_modules(fs: &dyn Fs, path: &Path) -> anyhow::Result<bool> {
524 let possible_node_modules_location = path.join("node_modules").join(PRETTIER_PACKAGE_NAME);
525 if let Some(node_modules_location_metadata) = fs
526 .metadata(&possible_node_modules_location)
527 .await
528 .with_context(|| format!("fetching metadata for {possible_node_modules_location:?}"))?
529 {
530 return Ok(node_modules_location_metadata.is_dir);
531 }
532 Ok(false)
533}
534
535async fn read_package_json(
536 fs: &dyn Fs,
537 path: &Path,
538) -> anyhow::Result<Option<HashMap<String, serde_json::Value>>> {
539 let possible_package_json = path.join("package.json");
540 if let Some(package_json_metadata) = fs
541 .metadata(&possible_package_json)
542 .await
543 .with_context(|| format!("fetching metadata for package json {possible_package_json:?}"))?
544 {
545 if !package_json_metadata.is_dir && !package_json_metadata.is_symlink {
546 let package_json_contents = fs
547 .load(&possible_package_json)
548 .await
549 .with_context(|| format!("reading {possible_package_json:?} file contents"))?;
550 return serde_json::from_str::<HashMap<String, serde_json::Value>>(
551 &package_json_contents,
552 )
553 .map(Some)
554 .with_context(|| format!("parsing {possible_package_json:?} file contents"));
555 }
556 }
557 Ok(None)
558}
559
560enum Format {}
561
562#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
563#[serde(rename_all = "camelCase")]
564struct FormatParams {
565 text: String,
566 options: FormatOptions,
567}
568
569#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
570#[serde(rename_all = "camelCase")]
571struct FormatOptions {
572 plugins: Vec<PathBuf>,
573 parser: Option<String>,
574 #[serde(rename = "filepath")]
575 path: Option<PathBuf>,
576 prettier_options: Option<HashMap<String, serde_json::Value>>,
577 ignore_path: Option<PathBuf>,
578}
579
580#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
581#[serde(rename_all = "camelCase")]
582struct FormatResult {
583 text: String,
584}
585
586impl lsp::request::Request for Format {
587 type Params = FormatParams;
588 type Result = FormatResult;
589 const METHOD: &'static str = "prettier/format";
590}
591
592enum ClearCache {}
593
594impl lsp::request::Request for ClearCache {
595 type Params = ();
596 type Result = ();
597 const METHOD: &'static str = "prettier/clear_cache";
598}
599
600#[cfg(test)]
601mod tests {
602 use fs::FakeFs;
603 use serde_json::json;
604
605 use super::*;
606
607 #[gpui::test]
608 async fn test_prettier_lookup_finds_nothing(cx: &mut gpui::TestAppContext) {
609 let fs = FakeFs::new(cx.executor());
610 fs.insert_tree(
611 "/root",
612 json!({
613 ".config": {
614 "zed": {
615 "settings.json": r#"{ "formatter": "auto" }"#,
616 },
617 },
618 "work": {
619 "project": {
620 "src": {
621 "index.js": "// index.js file contents",
622 },
623 "node_modules": {
624 "expect": {
625 "build": {
626 "print.js": "// print.js file contents",
627 },
628 "package.json": r#"{
629 "devDependencies": {
630 "prettier": "2.5.1"
631 }
632 }"#,
633 },
634 "prettier": {
635 "index.js": "// Dummy prettier package file",
636 },
637 },
638 "package.json": r#"{}"#
639 },
640 }
641 }),
642 )
643 .await;
644
645 assert_eq!(
646 Prettier::locate_prettier_installation(
647 fs.as_ref(),
648 &HashSet::default(),
649 Path::new("/root/.config/zed/settings.json"),
650 )
651 .await
652 .unwrap(),
653 ControlFlow::Continue(None),
654 "Should find no prettier for path hierarchy without it"
655 );
656 assert_eq!(
657 Prettier::locate_prettier_installation(
658 fs.as_ref(),
659 &HashSet::default(),
660 Path::new("/root/work/project/src/index.js")
661 )
662 .await
663 .unwrap(),
664 ControlFlow::Continue(Some(PathBuf::from("/root/work/project"))),
665 "Should successfully find a prettier for path hierarchy that has node_modules with prettier, but no package.json mentions of it"
666 );
667 assert_eq!(
668 Prettier::locate_prettier_installation(
669 fs.as_ref(),
670 &HashSet::default(),
671 Path::new("/root/work/project/node_modules/expect/build/print.js")
672 )
673 .await
674 .unwrap(),
675 ControlFlow::Break(()),
676 "Should not format files inside node_modules/"
677 );
678 }
679
680 #[gpui::test]
681 async fn test_prettier_lookup_in_simple_npm_projects(cx: &mut gpui::TestAppContext) {
682 let fs = FakeFs::new(cx.executor());
683 fs.insert_tree(
684 "/root",
685 json!({
686 "web_blog": {
687 "node_modules": {
688 "prettier": {
689 "index.js": "// Dummy prettier package file",
690 },
691 "expect": {
692 "build": {
693 "print.js": "// print.js file contents",
694 },
695 "package.json": r#"{
696 "devDependencies": {
697 "prettier": "2.5.1"
698 }
699 }"#,
700 },
701 },
702 "pages": {
703 "[slug].tsx": "// [slug].tsx file contents",
704 },
705 "package.json": r#"{
706 "devDependencies": {
707 "prettier": "2.3.0"
708 },
709 "prettier": {
710 "semi": false,
711 "printWidth": 80,
712 "htmlWhitespaceSensitivity": "strict",
713 "tabWidth": 4
714 }
715 }"#
716 }
717 }),
718 )
719 .await;
720
721 assert_eq!(
722 Prettier::locate_prettier_installation(
723 fs.as_ref(),
724 &HashSet::default(),
725 Path::new("/root/web_blog/pages/[slug].tsx")
726 )
727 .await
728 .unwrap(),
729 ControlFlow::Continue(Some(PathBuf::from("/root/web_blog"))),
730 "Should find a preinstalled prettier in the project root"
731 );
732 assert_eq!(
733 Prettier::locate_prettier_installation(
734 fs.as_ref(),
735 &HashSet::default(),
736 Path::new("/root/web_blog/node_modules/expect/build/print.js")
737 )
738 .await
739 .unwrap(),
740 ControlFlow::Break(()),
741 "Should not allow formatting node_modules/ contents"
742 );
743 }
744
745 #[gpui::test]
746 async fn test_prettier_lookup_for_not_installed(cx: &mut gpui::TestAppContext) {
747 let fs = FakeFs::new(cx.executor());
748 fs.insert_tree(
749 "/root",
750 json!({
751 "work": {
752 "web_blog": {
753 "node_modules": {
754 "expect": {
755 "build": {
756 "print.js": "// print.js file contents",
757 },
758 "package.json": r#"{
759 "devDependencies": {
760 "prettier": "2.5.1"
761 }
762 }"#,
763 },
764 },
765 "pages": {
766 "[slug].tsx": "// [slug].tsx file contents",
767 },
768 "package.json": r#"{
769 "devDependencies": {
770 "prettier": "2.3.0"
771 },
772 "prettier": {
773 "semi": false,
774 "printWidth": 80,
775 "htmlWhitespaceSensitivity": "strict",
776 "tabWidth": 4
777 }
778 }"#
779 }
780 }
781 }),
782 )
783 .await;
784
785 assert_eq!(
786 Prettier::locate_prettier_installation(
787 fs.as_ref(),
788 &HashSet::default(),
789 Path::new("/root/work/web_blog/pages/[slug].tsx")
790 )
791 .await
792 .unwrap(),
793 ControlFlow::Continue(None),
794 "Should find no prettier when node_modules don't have it"
795 );
796
797 assert_eq!(
798 Prettier::locate_prettier_installation(
799 fs.as_ref(),
800 &HashSet::from_iter(
801 [PathBuf::from("/root"), PathBuf::from("/root/work")].into_iter()
802 ),
803 Path::new("/root/work/web_blog/pages/[slug].tsx")
804 )
805 .await
806 .unwrap(),
807 ControlFlow::Continue(Some(PathBuf::from("/root/work"))),
808 "Should return closest cached value found without path checks"
809 );
810
811 assert_eq!(
812 Prettier::locate_prettier_installation(
813 fs.as_ref(),
814 &HashSet::default(),
815 Path::new("/root/work/web_blog/node_modules/expect/build/print.js")
816 )
817 .await
818 .unwrap(),
819 ControlFlow::Break(()),
820 "Should not allow formatting files inside node_modules/"
821 );
822 assert_eq!(
823 Prettier::locate_prettier_installation(
824 fs.as_ref(),
825 &HashSet::from_iter(
826 [PathBuf::from("/root"), PathBuf::from("/root/work")].into_iter()
827 ),
828 Path::new("/root/work/web_blog/node_modules/expect/build/print.js")
829 )
830 .await
831 .unwrap(),
832 ControlFlow::Break(()),
833 "Should ignore cache lookup for files inside node_modules/"
834 );
835 }
836
837 #[gpui::test]
838 async fn test_prettier_lookup_in_npm_workspaces(cx: &mut gpui::TestAppContext) {
839 let fs = FakeFs::new(cx.executor());
840 fs.insert_tree(
841 "/root",
842 json!({
843 "work": {
844 "full-stack-foundations": {
845 "exercises": {
846 "03.loading": {
847 "01.problem.loader": {
848 "app": {
849 "routes": {
850 "users+": {
851 "$username_+": {
852 "notes.tsx": "// notes.tsx file contents",
853 },
854 },
855 },
856 },
857 "node_modules": {
858 "test.js": "// test.js contents",
859 },
860 "package.json": r#"{
861 "devDependencies": {
862 "prettier": "^3.0.3"
863 }
864 }"#
865 },
866 },
867 },
868 "package.json": r#"{
869 "workspaces": ["exercises/*/*", "examples/*"]
870 }"#,
871 "node_modules": {
872 "prettier": {
873 "index.js": "// Dummy prettier package file",
874 },
875 },
876 },
877 }
878 }),
879 )
880 .await;
881
882 assert_eq!(
883 Prettier::locate_prettier_installation(
884 fs.as_ref(),
885 &HashSet::default(),
886 Path::new("/root/work/full-stack-foundations/exercises/03.loading/01.problem.loader/app/routes/users+/$username_+/notes.tsx"),
887 ).await.unwrap(),
888 ControlFlow::Continue(Some(PathBuf::from("/root/work/full-stack-foundations"))),
889 "Should ascend to the multi-workspace root and find the prettier there",
890 );
891
892 assert_eq!(
893 Prettier::locate_prettier_installation(
894 fs.as_ref(),
895 &HashSet::default(),
896 Path::new("/root/work/full-stack-foundations/node_modules/prettier/index.js")
897 )
898 .await
899 .unwrap(),
900 ControlFlow::Break(()),
901 "Should not allow formatting files inside root node_modules/"
902 );
903 assert_eq!(
904 Prettier::locate_prettier_installation(
905 fs.as_ref(),
906 &HashSet::default(),
907 Path::new("/root/work/full-stack-foundations/exercises/03.loading/01.problem.loader/node_modules/test.js")
908 )
909 .await
910 .unwrap(),
911 ControlFlow::Break(()),
912 "Should not allow formatting files inside submodule's node_modules/"
913 );
914 }
915
916 #[gpui::test]
917 async fn test_prettier_lookup_in_npm_workspaces_for_not_installed(
918 cx: &mut gpui::TestAppContext,
919 ) {
920 let fs = FakeFs::new(cx.executor());
921 fs.insert_tree(
922 "/root",
923 json!({
924 "work": {
925 "full-stack-foundations": {
926 "exercises": {
927 "03.loading": {
928 "01.problem.loader": {
929 "app": {
930 "routes": {
931 "users+": {
932 "$username_+": {
933 "notes.tsx": "// notes.tsx file contents",
934 },
935 },
936 },
937 },
938 "node_modules": {},
939 "package.json": r#"{
940 "devDependencies": {
941 "prettier": "^3.0.3"
942 }
943 }"#
944 },
945 },
946 },
947 "package.json": r#"{
948 "workspaces": ["exercises/*/*", "examples/*"]
949 }"#,
950 },
951 }
952 }),
953 )
954 .await;
955
956 match Prettier::locate_prettier_installation(
957 fs.as_ref(),
958 &HashSet::default(),
959 Path::new("/root/work/full-stack-foundations/exercises/03.loading/01.problem.loader/app/routes/users+/$username_+/notes.tsx")
960 )
961 .await {
962 Ok(path) => panic!("Expected to fail for prettier in package.json but not in node_modules found, but got path {path:?}"),
963 Err(e) => {
964 let message = e.to_string().replace("\\\\", "/");
965 assert!(message.contains("/root/work/full-stack-foundations/exercises/03.loading/01.problem.loader"), "Error message should mention which project had prettier defined");
966 assert!(message.contains("/root/work/full-stack-foundations"), "Error message should mention potential candidates without prettier node_modules contents");
967 },
968 };
969 }
970
971 #[gpui::test]
972 async fn test_prettier_ignore_with_editor_prettier(cx: &mut gpui::TestAppContext) {
973 let fs = FakeFs::new(cx.executor());
974 fs.insert_tree(
975 "/root",
976 json!({
977 "project": {
978 "src": {
979 "index.js": "// index.js file contents",
980 "ignored.js": "// this file should be ignored",
981 },
982 ".prettierignore": "ignored.js",
983 "package.json": r#"{
984 "name": "test-project"
985 }"#
986 }
987 }),
988 )
989 .await;
990
991 assert_eq!(
992 Prettier::locate_prettier_ignore(
993 fs.as_ref(),
994 &HashSet::default(),
995 Path::new("/root/project/src/index.js"),
996 )
997 .await
998 .unwrap(),
999 ControlFlow::Continue(Some(PathBuf::from("/root/project"))),
1000 "Should find prettierignore in project root"
1001 );
1002 }
1003
1004 #[gpui::test]
1005 async fn test_prettier_ignore_in_monorepo_with_only_child_ignore(
1006 cx: &mut gpui::TestAppContext,
1007 ) {
1008 let fs = FakeFs::new(cx.executor());
1009 fs.insert_tree(
1010 "/root",
1011 json!({
1012 "monorepo": {
1013 "node_modules": {
1014 "prettier": {
1015 "index.js": "// Dummy prettier package file",
1016 }
1017 },
1018 "packages": {
1019 "web": {
1020 "src": {
1021 "index.js": "// index.js contents",
1022 "ignored.js": "// this should be ignored",
1023 },
1024 ".prettierignore": "ignored.js",
1025 "package.json": r#"{
1026 "name": "web-package"
1027 }"#
1028 }
1029 },
1030 "package.json": r#"{
1031 "workspaces": ["packages/*"],
1032 "devDependencies": {
1033 "prettier": "^2.0.0"
1034 }
1035 }"#
1036 }
1037 }),
1038 )
1039 .await;
1040
1041 assert_eq!(
1042 Prettier::locate_prettier_ignore(
1043 fs.as_ref(),
1044 &HashSet::default(),
1045 Path::new("/root/monorepo/packages/web/src/index.js"),
1046 )
1047 .await
1048 .unwrap(),
1049 ControlFlow::Continue(Some(PathBuf::from("/root/monorepo/packages/web"))),
1050 "Should find prettierignore in child package"
1051 );
1052 }
1053
1054 #[gpui::test]
1055 async fn test_prettier_ignore_in_monorepo_with_root_and_child_ignores(
1056 cx: &mut gpui::TestAppContext,
1057 ) {
1058 let fs = FakeFs::new(cx.executor());
1059 fs.insert_tree(
1060 "/root",
1061 json!({
1062 "monorepo": {
1063 "node_modules": {
1064 "prettier": {
1065 "index.js": "// Dummy prettier package file",
1066 }
1067 },
1068 ".prettierignore": "main.js",
1069 "packages": {
1070 "web": {
1071 "src": {
1072 "main.js": "// this should not be ignored",
1073 "ignored.js": "// this should be ignored",
1074 },
1075 ".prettierignore": "ignored.js",
1076 "package.json": r#"{
1077 "name": "web-package"
1078 }"#
1079 }
1080 },
1081 "package.json": r#"{
1082 "workspaces": ["packages/*"],
1083 "devDependencies": {
1084 "prettier": "^2.0.0"
1085 }
1086 }"#
1087 }
1088 }),
1089 )
1090 .await;
1091
1092 assert_eq!(
1093 Prettier::locate_prettier_ignore(
1094 fs.as_ref(),
1095 &HashSet::default(),
1096 Path::new("/root/monorepo/packages/web/src/main.js"),
1097 )
1098 .await
1099 .unwrap(),
1100 ControlFlow::Continue(Some(PathBuf::from("/root/monorepo/packages/web"))),
1101 "Should find child package prettierignore first"
1102 );
1103
1104 assert_eq!(
1105 Prettier::locate_prettier_ignore(
1106 fs.as_ref(),
1107 &HashSet::default(),
1108 Path::new("/root/monorepo/packages/web/src/ignored.js"),
1109 )
1110 .await
1111 .unwrap(),
1112 ControlFlow::Continue(Some(PathBuf::from("/root/monorepo/packages/web"))),
1113 "Should find child package prettierignore first"
1114 );
1115 }
1116}