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