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