1use std::collections::VecDeque;
2use std::path::{Path, PathBuf};
3use std::sync::Arc;
4
5use anyhow::Context;
6use client::{proto, Client};
7use collections::{HashMap, HashSet};
8use fs::Fs;
9use gpui::{AsyncAppContext, ModelHandle};
10use language::language_settings::language_settings;
11use language::proto::deserialize_diff;
12use language::{Buffer, BundledFormatter, Diff};
13use lsp::request::Request;
14use lsp::{LanguageServer, LanguageServerId};
15use node_runtime::NodeRuntime;
16use serde::{Deserialize, Serialize};
17use util::paths::DEFAULT_PRETTIER_DIR;
18
19pub enum Prettier {
20 Local(Local),
21 Remote(Remote),
22 #[cfg(any(test, feature = "test-support"))]
23 Test(TestPrettier),
24}
25
26pub struct Local {
27 worktree_id: Option<usize>,
28 default: bool,
29 prettier_dir: PathBuf,
30 server: Arc<LanguageServer>,
31}
32
33pub struct Remote {
34 project_id: u64,
35 worktree_id: Option<usize>,
36 prettier_dir: PathBuf,
37 client: Arc<Client>,
38}
39
40#[cfg(any(test, feature = "test-support"))]
41pub struct TestPrettier {
42 worktree_id: Option<usize>,
43 prettier_dir: PathBuf,
44 default: bool,
45}
46
47#[derive(Debug)]
48pub struct LocateStart {
49 pub worktree_root_path: Arc<Path>,
50 pub starting_path: Arc<Path>,
51}
52
53pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js";
54pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js");
55const PRETTIER_PACKAGE_NAME: &str = "prettier";
56const TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME: &str = "prettier-plugin-tailwindcss";
57
58impl Prettier {
59 pub const CONFIG_FILE_NAMES: &'static [&'static str] = &[
60 ".prettierrc",
61 ".prettierrc.json",
62 ".prettierrc.json5",
63 ".prettierrc.yaml",
64 ".prettierrc.yml",
65 ".prettierrc.toml",
66 ".prettierrc.js",
67 ".prettierrc.cjs",
68 "package.json",
69 "prettier.config.js",
70 "prettier.config.cjs",
71 ".editorconfig",
72 ];
73
74 pub fn remote(
75 project_id: u64,
76 worktree_id: Option<usize>,
77 prettier_dir: PathBuf,
78 client: Arc<Client>,
79 ) -> Self {
80 Self::Remote(Remote {
81 project_id,
82 worktree_id,
83 prettier_dir,
84 client,
85 })
86 }
87
88 pub async fn locate(
89 starting_path: Option<LocateStart>,
90 fs: Arc<dyn Fs>,
91 ) -> anyhow::Result<PathBuf> {
92 let paths_to_check = match starting_path.as_ref() {
93 Some(starting_path) => {
94 let worktree_root = starting_path
95 .worktree_root_path
96 .components()
97 .into_iter()
98 .take_while(|path_component| {
99 path_component.as_os_str().to_string_lossy() != "node_modules"
100 })
101 .collect::<PathBuf>();
102
103 if worktree_root != starting_path.worktree_root_path.as_ref() {
104 vec![worktree_root]
105 } else {
106 let (worktree_root_metadata, start_path_metadata) = if starting_path
107 .starting_path
108 .as_ref()
109 == Path::new("")
110 {
111 let worktree_root_data =
112 fs.metadata(&worktree_root).await.with_context(|| {
113 format!(
114 "FS metadata fetch for worktree root path {worktree_root:?}",
115 )
116 })?;
117 (worktree_root_data.unwrap_or_else(|| {
118 panic!("cannot query prettier for non existing worktree root at {worktree_root_data:?}")
119 }), None)
120 } else {
121 let full_starting_path = worktree_root.join(&starting_path.starting_path);
122 let (worktree_root_data, start_path_data) = futures::try_join!(
123 fs.metadata(&worktree_root),
124 fs.metadata(&full_starting_path),
125 )
126 .with_context(|| {
127 format!("FS metadata fetch for starting path {full_starting_path:?}",)
128 })?;
129 (
130 worktree_root_data.unwrap_or_else(|| {
131 panic!("cannot query prettier for non existing worktree root at {worktree_root_data:?}")
132 }),
133 start_path_data,
134 )
135 };
136
137 match start_path_metadata {
138 Some(start_path_metadata) => {
139 anyhow::ensure!(worktree_root_metadata.is_dir,
140 "For non-empty start path, worktree root {starting_path:?} should be a directory");
141 anyhow::ensure!(
142 !start_path_metadata.is_dir,
143 "For non-empty start path, it should not be a directory {starting_path:?}"
144 );
145 anyhow::ensure!(
146 !start_path_metadata.is_symlink,
147 "For non-empty start path, it should not be a symlink {starting_path:?}"
148 );
149
150 let file_to_format = starting_path.starting_path.as_ref();
151 let mut paths_to_check = VecDeque::from(vec![worktree_root.clone()]);
152 let mut current_path = worktree_root;
153 for path_component in file_to_format.components().into_iter() {
154 current_path = current_path.join(path_component);
155 paths_to_check.push_front(current_path.clone());
156 if path_component.as_os_str().to_string_lossy() == "node_modules" {
157 break;
158 }
159 }
160 paths_to_check.pop_front(); // last one is the file itself or node_modules, skip it
161 Vec::from(paths_to_check)
162 }
163 None => {
164 anyhow::ensure!(
165 !worktree_root_metadata.is_dir,
166 "For empty start path, worktree root should not be a directory {starting_path:?}"
167 );
168 anyhow::ensure!(
169 !worktree_root_metadata.is_symlink,
170 "For empty start path, worktree root should not be a symlink {starting_path:?}"
171 );
172 worktree_root
173 .parent()
174 .map(|path| vec![path.to_path_buf()])
175 .unwrap_or_default()
176 }
177 }
178 }
179 }
180 None => Vec::new(),
181 };
182
183 match find_closest_prettier_dir(paths_to_check, fs.as_ref())
184 .await
185 .with_context(|| format!("finding prettier starting with {starting_path:?}"))?
186 {
187 Some(prettier_dir) => Ok(prettier_dir),
188 None => Ok(DEFAULT_PRETTIER_DIR.to_path_buf()),
189 }
190 }
191
192 #[cfg(any(test, feature = "test-support"))]
193 pub async fn start(
194 worktree_id: Option<usize>,
195 _: LanguageServerId,
196 prettier_dir: PathBuf,
197 _: Arc<dyn NodeRuntime>,
198 _: AsyncAppContext,
199 ) -> anyhow::Result<Self> {
200 Ok(
201 #[cfg(any(test, feature = "test-support"))]
202 Self::Test(TestPrettier {
203 worktree_id,
204 default: prettier_dir == DEFAULT_PRETTIER_DIR.as_path(),
205 prettier_dir,
206 }),
207 )
208 }
209
210 #[cfg(not(any(test, feature = "test-support")))]
211 pub async fn start(
212 worktree_id: Option<usize>,
213 server_id: LanguageServerId,
214 prettier_dir: PathBuf,
215 node: Arc<dyn NodeRuntime>,
216 cx: AsyncAppContext,
217 ) -> anyhow::Result<Self> {
218 use lsp::LanguageServerBinary;
219
220 let backgroud = cx.background();
221 anyhow::ensure!(
222 prettier_dir.is_dir(),
223 "Prettier dir {prettier_dir:?} is not a directory"
224 );
225 let prettier_server = DEFAULT_PRETTIER_DIR.join(PRETTIER_SERVER_FILE);
226 anyhow::ensure!(
227 prettier_server.is_file(),
228 "no prettier server package found at {prettier_server:?}"
229 );
230
231 let node_path = backgroud
232 .spawn(async move { node.binary_path().await })
233 .await?;
234 let server = LanguageServer::new(
235 server_id,
236 LanguageServerBinary {
237 path: node_path,
238 arguments: vec![prettier_server.into(), prettier_dir.as_path().into()],
239 },
240 Path::new("/"),
241 None,
242 cx,
243 )
244 .context("prettier server creation")?;
245 let server = backgroud
246 .spawn(server.initialize(None))
247 .await
248 .context("prettier server initialization")?;
249 Ok(Self::Local(Local {
250 worktree_id,
251 server,
252 default: prettier_dir == DEFAULT_PRETTIER_DIR.as_path(),
253 prettier_dir,
254 }))
255 }
256
257 pub async fn invoke(
258 &self,
259 buffer: Option<&ModelHandle<Buffer>>,
260 buffer_path: Option<PathBuf>,
261 method: &str,
262 cx: &AsyncAppContext,
263 ) -> anyhow::Result<Option<Diff>> {
264 match method {
265 Format::METHOD => self
266 .format(
267 buffer.expect("missing buffer for format invocation"),
268 buffer_path,
269 cx,
270 )
271 .await
272 .context("invoke method")
273 .map(Some),
274 ClearCache::METHOD => {
275 self.clear_cache().await.context("invoke method")?;
276 Ok(None)
277 }
278 unknown => anyhow::bail!("Unknown method {unknown}"),
279 }
280 }
281
282 pub async fn format(
283 &self,
284 buffer: &ModelHandle<Buffer>,
285 buffer_path: Option<PathBuf>,
286 cx: &AsyncAppContext,
287 ) -> anyhow::Result<Diff> {
288 match self {
289 Self::Local(local) => {
290 let params = buffer.read_with(cx, |buffer, cx| {
291 let buffer_language = buffer.language();
292 let parsers_with_plugins = buffer_language
293 .into_iter()
294 .flat_map(|language| {
295 language
296 .lsp_adapters()
297 .iter()
298 .flat_map(|adapter| adapter.enabled_formatters())
299 .filter_map(|formatter| match formatter {
300 BundledFormatter::Prettier {
301 parser_name,
302 plugin_names,
303 } => Some((parser_name, plugin_names)),
304 })
305 })
306 .fold(
307 HashMap::default(),
308 |mut parsers_with_plugins, (parser_name, plugins)| {
309 match parser_name {
310 Some(parser_name) => parsers_with_plugins
311 .entry(parser_name)
312 .or_insert_with(HashSet::default)
313 .extend(plugins),
314 None => parsers_with_plugins.values_mut().for_each(|existing_plugins| {
315 existing_plugins.extend(plugins.iter());
316 }),
317 }
318 parsers_with_plugins
319 },
320 );
321
322 let selected_parser_with_plugins = parsers_with_plugins.iter().max_by_key(|(_, plugins)| plugins.len());
323 if parsers_with_plugins.len() > 1 {
324 log::warn!("Found multiple parsers with plugins {parsers_with_plugins:?}, will select only one: {selected_parser_with_plugins:?}");
325 }
326
327 let prettier_node_modules = self.prettier_dir().join("node_modules");
328 anyhow::ensure!(prettier_node_modules.is_dir(), "Prettier node_modules dir does not exist: {prettier_node_modules:?}");
329 let plugin_name_into_path = |plugin_name: &str| {
330 let prettier_plugin_dir = prettier_node_modules.join(plugin_name);
331 for possible_plugin_path in [
332 prettier_plugin_dir.join("dist").join("index.mjs"),
333 prettier_plugin_dir.join("index.mjs"),
334 prettier_plugin_dir.join("plugin.js"),
335 prettier_plugin_dir.join("index.js"),
336 prettier_plugin_dir,
337 ] {
338 if possible_plugin_path.is_file() {
339 return Some(possible_plugin_path);
340 }
341 }
342 None
343 };
344 let (parser, located_plugins) = match selected_parser_with_plugins {
345 Some((parser, plugins)) => {
346 // Tailwind plugin requires being added last
347 // https://github.com/tailwindlabs/prettier-plugin-tailwindcss#compatibility-with-other-prettier-plugins
348 let mut add_tailwind_back = false;
349
350 let mut plugins = plugins.into_iter().filter(|&&plugin_name| {
351 if plugin_name == TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME {
352 add_tailwind_back = true;
353 false
354 } else {
355 true
356 }
357 }).map(|plugin_name| (plugin_name, plugin_name_into_path(plugin_name))).collect::<Vec<_>>();
358 if add_tailwind_back {
359 plugins.push((&TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME, plugin_name_into_path(TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME)));
360 }
361 (Some(parser.to_string()), plugins)
362 },
363 None => (None, Vec::new()),
364 };
365
366 let prettier_options = if self.is_default() {
367 let language_settings = language_settings(buffer_language, buffer.file(), cx);
368 let mut options = language_settings.prettier.clone();
369 if !options.contains_key("tabWidth") {
370 options.insert(
371 "tabWidth".to_string(),
372 serde_json::Value::Number(serde_json::Number::from(
373 language_settings.tab_size.get(),
374 )),
375 );
376 }
377 if !options.contains_key("printWidth") {
378 options.insert(
379 "printWidth".to_string(),
380 serde_json::Value::Number(serde_json::Number::from(
381 language_settings.preferred_line_length,
382 )),
383 );
384 }
385 Some(options)
386 } else {
387 None
388 };
389
390 let plugins = located_plugins.into_iter().filter_map(|(plugin_name, located_plugin_path)| {
391 match located_plugin_path {
392 Some(path) => Some(path),
393 None => {
394 log::error!("Have not found plugin path for {plugin_name:?} inside {prettier_node_modules:?}");
395 None},
396 }
397 }).collect();
398 log::debug!("Formatting file {:?} with prettier, plugins :{plugins:?}, options: {prettier_options:?}", buffer.file().map(|f| f.full_path(cx)));
399
400 anyhow::Ok(FormatParams {
401 text: buffer.text(),
402 options: FormatOptions {
403 parser,
404 plugins,
405 path: buffer_path,
406 prettier_options,
407 },
408 })
409 }).context("prettier params calculation")?;
410 let response = local
411 .server
412 .request::<Format>(params)
413 .await
414 .context("prettier format request")?;
415 let diff_task = buffer.read_with(cx, |buffer, cx| buffer.diff(response.text, cx));
416 Ok(diff_task.await)
417 }
418 Self::Remote(remote) => buffer
419 .read_with(cx, |buffer, _| {
420 remote.client.request(proto::InvokePrettierForBuffer {
421 buffer_id: Some(buffer.remote_id()),
422 worktree_id: self.worktree_id().map(|id| id as u64),
423 method: Format::METHOD.to_string(),
424 project_id: remote.project_id,
425 prettier_path: remote.prettier_dir.to_string_lossy().to_string(),
426 })
427 })
428 .await
429 .context("prettier diff invoke")?
430 .diff
431 .map(deserialize_diff)
432 .context("missing diff after prettier diff invocation"),
433 #[cfg(any(test, feature = "test-support"))]
434 Self::Test(_) => Ok(buffer
435 .read_with(cx, |buffer, cx| {
436 let formatted_text = buffer.text() + "\nformatted by test prettier";
437 buffer.diff(formatted_text, cx)
438 })
439 .await),
440 }
441 }
442
443 pub async fn clear_cache(&self) -> anyhow::Result<()> {
444 match self {
445 Self::Local(local) => local
446 .server
447 .request::<ClearCache>(())
448 .await
449 .context("prettier clear cache"),
450 Self::Remote(remote) => remote
451 .client
452 .request(proto::InvokePrettierForBuffer {
453 buffer_id: None,
454 worktree_id: self.worktree_id().map(|id| id as u64),
455 method: ClearCache::METHOD.to_string(),
456 project_id: remote.project_id,
457 prettier_path: remote.prettier_dir.to_string_lossy().to_string(),
458 })
459 .await
460 .map(|response| {
461 debug_assert!(
462 response.diff.is_none(),
463 "Cleare cache invocation returned diff data"
464 )
465 })
466 .context("prettier invoke clear cache"),
467 #[cfg(any(test, feature = "test-support"))]
468 Self::Test(_) => Ok(()),
469 }
470 }
471
472 pub fn server(&self) -> Option<&Arc<LanguageServer>> {
473 match self {
474 Self::Local(local) => Some(&local.server),
475 Self::Remote(_) => None,
476 #[cfg(any(test, feature = "test-support"))]
477 Self::Test(_) => None,
478 }
479 }
480
481 pub fn is_default(&self) -> bool {
482 match self {
483 Self::Local(local) => local.default,
484 Self::Remote(_) => false,
485 #[cfg(any(test, feature = "test-support"))]
486 Self::Test(test_prettier) => test_prettier.default,
487 }
488 }
489
490 pub fn prettier_dir(&self) -> &Path {
491 match self {
492 Self::Local(local) => &local.prettier_dir,
493 Self::Remote(remote) => &remote.prettier_dir,
494 #[cfg(any(test, feature = "test-support"))]
495 Self::Test(test_prettier) => &test_prettier.prettier_dir,
496 }
497 }
498
499 pub fn worktree_id(&self) -> Option<usize> {
500 match self {
501 Self::Local(local) => local.worktree_id,
502 Self::Remote(remote) => remote.worktree_id,
503 #[cfg(any(test, feature = "test-support"))]
504 Self::Test(test_prettier) => test_prettier.worktree_id,
505 }
506 }
507}
508
509async fn find_closest_prettier_dir(
510 paths_to_check: Vec<PathBuf>,
511 fs: &dyn Fs,
512) -> anyhow::Result<Option<PathBuf>> {
513 for path in paths_to_check {
514 let possible_package_json = path.join("package.json");
515 if let Some(package_json_metadata) = fs
516 .metadata(&possible_package_json)
517 .await
518 .with_context(|| format!("Fetching metadata for {possible_package_json:?}"))?
519 {
520 if !package_json_metadata.is_dir && !package_json_metadata.is_symlink {
521 let package_json_contents = fs
522 .load(&possible_package_json)
523 .await
524 .with_context(|| format!("reading {possible_package_json:?} file contents"))?;
525 if let Ok(json_contents) = serde_json::from_str::<HashMap<String, serde_json::Value>>(
526 &package_json_contents,
527 ) {
528 if let Some(serde_json::Value::Object(o)) = json_contents.get("dependencies") {
529 if o.contains_key(PRETTIER_PACKAGE_NAME) {
530 return Ok(Some(path));
531 }
532 }
533 if let Some(serde_json::Value::Object(o)) = json_contents.get("devDependencies")
534 {
535 if o.contains_key(PRETTIER_PACKAGE_NAME) {
536 return Ok(Some(path));
537 }
538 }
539 }
540 }
541 }
542
543 let possible_node_modules_location = path.join("node_modules").join(PRETTIER_PACKAGE_NAME);
544 if let Some(node_modules_location_metadata) = fs
545 .metadata(&possible_node_modules_location)
546 .await
547 .with_context(|| format!("fetching metadata for {possible_node_modules_location:?}"))?
548 {
549 if node_modules_location_metadata.is_dir {
550 return Ok(Some(path));
551 }
552 }
553 }
554 Ok(None)
555}
556
557enum Format {}
558
559#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
560#[serde(rename_all = "camelCase")]
561struct FormatParams {
562 text: String,
563 options: FormatOptions,
564}
565
566#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
567#[serde(rename_all = "camelCase")]
568struct FormatOptions {
569 plugins: Vec<PathBuf>,
570 parser: Option<String>,
571 #[serde(rename = "filepath")]
572 path: Option<PathBuf>,
573 prettier_options: Option<HashMap<String, serde_json::Value>>,
574}
575
576#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
577#[serde(rename_all = "camelCase")]
578struct FormatResult {
579 text: String,
580}
581
582impl lsp::request::Request for Format {
583 type Params = FormatParams;
584 type Result = FormatResult;
585 const METHOD: &'static str = "prettier/format";
586}
587
588enum ClearCache {}
589
590impl lsp::request::Request for ClearCache {
591 type Params = ();
592 type Result = ();
593 const METHOD: &'static str = "prettier/clear_cache";
594}