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