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