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