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