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