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