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