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