1use anyhow::Context as _;
2use collections::{HashMap, HashSet};
3use fs::Fs;
4use gpui::{AsyncApp, Entity};
5use language::language_settings::{LanguageSettings, PrettierSettings};
6use language::{Buffer, Diff, Language};
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 time::Duration,
16};
17use util::{
18 paths::{PathMatcher, PathStyle},
19 rel_path::RelPath,
20};
21
22#[derive(Debug, Clone)]
23pub enum Prettier {
24 Real(RealPrettier),
25 #[cfg(any(test, feature = "test-support"))]
26 Test(TestPrettier),
27}
28
29#[derive(Debug, Clone)]
30pub struct RealPrettier {
31 default: bool,
32 prettier_dir: PathBuf,
33 server: Arc<LanguageServer>,
34}
35
36#[cfg(any(test, feature = "test-support"))]
37#[derive(Debug, Clone)]
38pub struct TestPrettier {
39 prettier_dir: PathBuf,
40 default: bool,
41}
42
43pub const FAIL_THRESHOLD: usize = 4;
44pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js";
45pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js");
46const PRETTIER_PACKAGE_NAME: &str = "prettier";
47const TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME: &str = "prettier-plugin-tailwindcss";
48
49#[cfg(any(test, feature = "test-support"))]
50pub const FORMAT_SUFFIX: &str = "\nformatted by test prettier";
51
52impl Prettier {
53 pub const CONFIG_FILE_NAMES: &'static [&'static str] = &[
54 ".prettierrc",
55 ".prettierrc.json",
56 ".prettierrc.json5",
57 ".prettierrc.yaml",
58 ".prettierrc.yml",
59 ".prettierrc.toml",
60 ".prettierrc.js",
61 ".prettierrc.cjs",
62 ".prettierrc.mjs",
63 ".prettierrc.ts",
64 ".prettierrc.cts",
65 ".prettierrc.mts",
66 "package.json",
67 "prettier.config.js",
68 "prettier.config.cjs",
69 "prettier.config.mjs",
70 "prettier.config.ts",
71 "prettier.config.cts",
72 "prettier.config.mts",
73 ".editorconfig",
74 ".prettierignore",
75 ];
76
77 pub async fn locate_prettier_installation(
78 fs: &dyn Fs,
79 installed_prettiers: &HashSet<PathBuf>,
80 locate_from: &Path,
81 ) -> anyhow::Result<ControlFlow<(), Option<PathBuf>>> {
82 let mut path_to_check = locate_from
83 .components()
84 .take_while(|component| component.as_os_str().to_string_lossy() != "node_modules")
85 .collect::<PathBuf>();
86 if path_to_check != locate_from {
87 log::debug!(
88 "Skipping prettier location for path {path_to_check:?} that is inside node_modules"
89 );
90 return Ok(ControlFlow::Break(()));
91 }
92 let path_to_check_metadata = fs
93 .metadata(&path_to_check)
94 .await
95 .with_context(|| format!("failed to get metadata for initial path {path_to_check:?}"))?
96 .with_context(|| format!("empty metadata for initial path {path_to_check:?}"))?;
97 if !path_to_check_metadata.is_dir {
98 path_to_check.pop();
99 }
100
101 let mut closest_package_json_path = None;
102 loop {
103 if installed_prettiers.contains(&path_to_check) {
104 log::debug!("Found prettier path {path_to_check:?} in installed prettiers");
105 return Ok(ControlFlow::Continue(Some(path_to_check)));
106 } else if let Some(package_json_contents) =
107 read_package_json(fs, &path_to_check).await?
108 {
109 if has_prettier_in_node_modules(fs, &path_to_check).await? {
110 log::debug!("Found prettier path {path_to_check:?} in the node_modules");
111 return Ok(ControlFlow::Continue(Some(path_to_check)));
112 } else {
113 match &closest_package_json_path {
114 None => closest_package_json_path = Some(path_to_check.clone()),
115 Some(closest_package_json_path) => {
116 match package_json_contents.get("workspaces") {
117 Some(serde_json::Value::Array(workspaces)) => {
118 let subproject_path = closest_package_json_path.strip_prefix(&path_to_check).expect("traversing path parents, should be able to strip prefix");
119 if workspaces.iter().filter_map(|value| {
120 if let serde_json::Value::String(s) = value {
121 Some(s.clone())
122 } else {
123 log::warn!("Skipping non-string 'workspaces' value: {value:?}");
124 None
125 }
126 }).any(|workspace_definition| {
127 workspace_definition == subproject_path.to_string_lossy() || PathMatcher::new(&[workspace_definition], PathStyle::local()).ok().is_some_and(
128 |path_matcher| RelPath::new(subproject_path, PathStyle::local()).is_ok_and(|path| path_matcher.is_match(path)))
129 }) {
130 anyhow::ensure!(has_prettier_in_node_modules(fs, &path_to_check).await?,
131 "Path {path_to_check:?} is the workspace root for project in \
132 {closest_package_json_path:?}, but it has no prettier installed"
133 );
134 log::info!(
135 "Found prettier path {path_to_check:?} in the workspace \
136 root for project in {closest_package_json_path:?}"
137 );
138 return Ok(ControlFlow::Continue(Some(path_to_check)));
139 } else {
140 log::warn!(
141 "Skipping path {path_to_check:?} workspace root with \
142 workspaces {workspaces:?} that have no prettier installed"
143 );
144 }
145 }
146 Some(unknown) => log::error!(
147 "Failed to parse workspaces for {path_to_check:?} from package.json, \
148 got {unknown:?}. Skipping."
149 ),
150 None => log::warn!(
151 "Skipping path {path_to_check:?} that has no prettier \
152 dependency and no workspaces section in its package.json"
153 ),
154 }
155 }
156 }
157 }
158 }
159
160 if !path_to_check.pop() {
161 log::debug!("Found no prettier in ancestors of {locate_from:?}");
162 return Ok(ControlFlow::Continue(None));
163 }
164 }
165 }
166
167 pub async fn locate_prettier_ignore(
168 fs: &dyn Fs,
169 prettier_ignores: &HashSet<PathBuf>,
170 locate_from: &Path,
171 ) -> anyhow::Result<ControlFlow<(), Option<PathBuf>>> {
172 let mut path_to_check = locate_from
173 .components()
174 .take_while(|component| component.as_os_str().to_string_lossy() != "node_modules")
175 .collect::<PathBuf>();
176 if path_to_check != locate_from {
177 log::debug!(
178 "Skipping prettier ignore location for path {path_to_check:?} that is inside node_modules"
179 );
180 return Ok(ControlFlow::Break(()));
181 }
182
183 let path_to_check_metadata = fs
184 .metadata(&path_to_check)
185 .await
186 .with_context(|| format!("failed to get metadata for initial path {path_to_check:?}"))?
187 .with_context(|| format!("empty metadata for initial path {path_to_check:?}"))?;
188 if !path_to_check_metadata.is_dir {
189 path_to_check.pop();
190 }
191
192 let mut closest_package_json_path = None;
193 loop {
194 if prettier_ignores.contains(&path_to_check) {
195 log::debug!("Found prettier ignore at {path_to_check:?}");
196 return Ok(ControlFlow::Continue(Some(path_to_check)));
197 } else if let Some(package_json_contents) =
198 read_package_json(fs, &path_to_check).await?
199 {
200 let ignore_path = path_to_check.join(".prettierignore");
201 if let Some(metadata) = fs
202 .metadata(&ignore_path)
203 .await
204 .with_context(|| format!("fetching metadata for {ignore_path:?}"))?
205 && !metadata.is_dir
206 && !metadata.is_symlink
207 {
208 log::info!("Found prettier ignore at {ignore_path:?}");
209 return Ok(ControlFlow::Continue(Some(path_to_check)));
210 }
211 match &closest_package_json_path {
212 None => closest_package_json_path = Some(path_to_check.clone()),
213 Some(closest_package_json_path) => {
214 if let Some(serde_json::Value::Array(workspaces)) =
215 package_json_contents.get("workspaces")
216 {
217 let subproject_path = closest_package_json_path
218 .strip_prefix(&path_to_check)
219 .expect("traversing path parents, should be able to strip prefix");
220
221 if workspaces
222 .iter()
223 .filter_map(|value| {
224 if let serde_json::Value::String(s) = value {
225 Some(s.clone())
226 } else {
227 log::warn!(
228 "Skipping non-string 'workspaces' value: {value:?}"
229 );
230 None
231 }
232 })
233 .any(|workspace_definition| {
234 workspace_definition == subproject_path.to_string_lossy()
235 || PathMatcher::new(
236 &[workspace_definition],
237 PathStyle::local(),
238 )
239 .ok()
240 .is_some_and(
241 |path_matcher| {
242 RelPath::new(subproject_path, PathStyle::local())
243 .is_ok_and(|rel_path| {
244 path_matcher.is_match(rel_path)
245 })
246 },
247 )
248 })
249 {
250 let workspace_ignore = path_to_check.join(".prettierignore");
251 if let Some(metadata) = fs.metadata(&workspace_ignore).await?
252 && !metadata.is_dir
253 {
254 log::info!(
255 "Found prettier ignore at workspace root {workspace_ignore:?}"
256 );
257 return Ok(ControlFlow::Continue(Some(path_to_check)));
258 }
259 }
260 }
261 }
262 }
263 }
264
265 if !path_to_check.pop() {
266 log::debug!("Found no prettier ignore in ancestors of {locate_from:?}");
267 return Ok(ControlFlow::Continue(None));
268 }
269 }
270 }
271
272 #[cfg(any(test, feature = "test-support"))]
273 pub async fn start(
274 _: LanguageServerId,
275 prettier_dir: PathBuf,
276 _: NodeRuntime,
277 _: Duration,
278 _: AsyncApp,
279 ) -> anyhow::Result<Self> {
280 Ok(Self::Test(TestPrettier {
281 default: prettier_dir == default_prettier_dir().as_path(),
282 prettier_dir,
283 }))
284 }
285
286 #[cfg(not(any(test, feature = "test-support")))]
287 pub async fn start(
288 server_id: LanguageServerId,
289 prettier_dir: PathBuf,
290 node: NodeRuntime,
291 request_timeout: Duration,
292 mut cx: AsyncApp,
293 ) -> anyhow::Result<Self> {
294 use lsp::{LanguageServerBinary, LanguageServerName};
295
296 let executor = cx.background_executor().clone();
297 anyhow::ensure!(
298 prettier_dir.is_dir(),
299 "Prettier dir {prettier_dir:?} is not a directory"
300 );
301 let prettier_server = default_prettier_dir().join(PRETTIER_SERVER_FILE);
302 anyhow::ensure!(
303 prettier_server.is_file(),
304 "no prettier server package found at {prettier_server:?}"
305 );
306
307 let node_path = executor
308 .spawn(async move { node.binary_path().await })
309 .await?;
310 let server_name = LanguageServerName("prettier".into());
311 let server_binary = LanguageServerBinary {
312 path: node_path,
313 arguments: vec![prettier_server.into(), prettier_dir.as_path().into()],
314 env: None,
315 };
316
317 let server = LanguageServer::new(
318 Arc::new(parking_lot::Mutex::new(None)),
319 server_id,
320 server_name,
321 server_binary,
322 &prettier_dir,
323 None,
324 Default::default(),
325 &mut cx,
326 )
327 .context("prettier server creation")?;
328
329 let server = cx
330 .update(|cx| {
331 let params = server.default_initialize_params(false, false, cx);
332 let configuration = lsp::DidChangeConfigurationParams {
333 settings: Default::default(),
334 };
335 executor.spawn(server.initialize(params, configuration.into(), request_timeout, cx))
336 })
337 .await
338 .context("prettier server initialization")?;
339 Ok(Self::Real(RealPrettier {
340 server,
341 default: prettier_dir == default_prettier_dir().as_path(),
342 prettier_dir,
343 }))
344 }
345
346 pub async fn format(
347 &self,
348 buffer: &Entity<Buffer>,
349 buffer_path: Option<PathBuf>,
350 ignore_dir: Option<PathBuf>,
351 request_timeout: Duration,
352 cx: &mut AsyncApp,
353 ) -> anyhow::Result<Diff> {
354 match self {
355 Self::Real(local) => {
356 let params = buffer
357 .update(cx, |buffer, cx| {
358 let buffer_language = buffer.language().map(|language| language.as_ref());
359 let language_settings = LanguageSettings::for_buffer(&buffer, cx);
360 let prettier_settings = &language_settings.prettier;
361 anyhow::ensure!(
362 prettier_settings.allowed,
363 "Cannot format: prettier is not allowed for language {buffer_language:?}"
364 );
365 let prettier_node_modules = self.prettier_dir().join("node_modules");
366 anyhow::ensure!(
367 prettier_node_modules.is_dir(),
368 "Prettier node_modules dir does not exist: {prettier_node_modules:?}"
369 );
370 let plugin_name_into_path = |plugin_name: &str| {
371 let prettier_plugin_dir = prettier_node_modules.join(plugin_name);
372 [
373 prettier_plugin_dir.join("dist").join("index.mjs"),
374 prettier_plugin_dir.join("dist").join("index.js"),
375 prettier_plugin_dir.join("dist").join("plugin.js"),
376 prettier_plugin_dir.join("src").join("plugin.js"),
377 prettier_plugin_dir.join("lib").join("index.js"),
378 prettier_plugin_dir.join("index.mjs"),
379 prettier_plugin_dir.join("index.js"),
380 prettier_plugin_dir.join("plugin.js"),
381 // this one is for @prettier/plugin-php
382 prettier_plugin_dir.join("standalone.js"),
383 // this one is for prettier-plugin-latex
384 prettier_plugin_dir.join("dist").join("prettier-plugin-latex.js"),
385 prettier_plugin_dir,
386 ]
387 .into_iter()
388 .find(|possible_plugin_path| possible_plugin_path.is_file())
389 };
390
391 // Tailwind plugin requires being added last
392 // https://github.com/tailwindlabs/prettier-plugin-tailwindcss#compatibility-with-other-prettier-plugins
393 let mut add_tailwind_back = false;
394
395 let mut located_plugins = prettier_settings.plugins.iter()
396 .filter(|plugin_name| {
397 if plugin_name.as_str() == TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME {
398 add_tailwind_back = true;
399 false
400 } else {
401 true
402 }
403 })
404 .map(|plugin_name| {
405 let plugin_path = plugin_name_into_path(plugin_name);
406 (plugin_name.clone(), plugin_path)
407 })
408 .collect::<Vec<_>>();
409 if add_tailwind_back {
410 located_plugins.push((
411 TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME.to_owned(),
412 plugin_name_into_path(TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME),
413 ));
414 }
415
416 let prettier_options = if self.is_default() {
417 let mut options = prettier_settings.options.clone();
418 if !options.contains_key("tabWidth") {
419 options.insert(
420 "tabWidth".to_string(),
421 serde_json::Value::Number(serde_json::Number::from(
422 language_settings.tab_size.get(),
423 )),
424 );
425 }
426 if !options.contains_key("printWidth") {
427 options.insert(
428 "printWidth".to_string(),
429 serde_json::Value::Number(serde_json::Number::from(
430 language_settings.preferred_line_length,
431 )),
432 );
433 }
434 if !options.contains_key("useTabs") {
435 options.insert(
436 "useTabs".to_string(),
437 serde_json::Value::Bool(language_settings.hard_tabs),
438 );
439 }
440 Some(options)
441 } else {
442 None
443 };
444
445 let plugins = located_plugins
446 .into_iter()
447 .filter_map(|(plugin_name, located_plugin_path)| {
448 match located_plugin_path {
449 Some(path) => Some(path),
450 None => {
451 log::error!("Have not found plugin path for {plugin_name:?} inside {prettier_node_modules:?}");
452 None
453 }
454 }
455 })
456 .collect();
457
458 let parser = prettier_parser_name(buffer_path.as_deref(), buffer_language, prettier_settings).context("getting prettier parser")?;
459
460 let ignore_path = ignore_dir.and_then(|dir| {
461 let ignore_file = dir.join(".prettierignore");
462 ignore_file.is_file().then_some(ignore_file)
463 });
464
465 log::debug!(
466 "Formatting file {:?} with prettier, plugins :{:?}, options: {:?}, ignore_path: {:?}",
467 buffer.file().map(|f| f.full_path(cx)),
468 plugins,
469 prettier_options,
470 ignore_path,
471 );
472
473 anyhow::Ok(FormatParams {
474 text: buffer.text(),
475 options: FormatOptions {
476 path: buffer_path,
477 parser,
478 plugins,
479 prettier_options,
480 ignore_path,
481 },
482 })
483 })
484 .context("building prettier request")?;
485
486 let response = local
487 .server
488 .request::<Format>(params, request_timeout)
489 .await
490 .into_response()?;
491 let diff_task = buffer.update(cx, |buffer, cx| buffer.diff(response.text, cx));
492 Ok(diff_task.await)
493 }
494 #[cfg(any(test, feature = "test-support"))]
495 Self::Test(_) => Ok(buffer
496 .update(cx, |buffer, cx| {
497 match buffer
498 .language()
499 .map(|language| language.lsp_id())
500 .as_deref()
501 {
502 Some("rust") => anyhow::bail!("prettier does not support Rust"),
503 Some(_other) => {
504 let mut formatted_text = buffer.text() + FORMAT_SUFFIX;
505
506 let buffer_language =
507 buffer.language().map(|language| language.as_ref());
508 let language_settings = LanguageSettings::for_buffer(buffer, cx);
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, request_timeout: Duration) -> anyhow::Result<()> {
530 match self {
531 Self::Real(local) => local
532 .server
533 .request::<ClearCache>((), request_timeout)
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}