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 buffer_path: Option<PathBuf>,
203 cx: &AsyncAppContext,
204 ) -> anyhow::Result<Diff> {
205 let params = buffer.read_with(cx, |buffer, cx| {
206 let buffer_language = buffer.language();
207 let parsers_with_plugins = buffer_language
208 .into_iter()
209 .flat_map(|language| {
210 language
211 .lsp_adapters()
212 .iter()
213 .flat_map(|adapter| adapter.enabled_formatters())
214 .filter_map(|formatter| match formatter {
215 BundledFormatter::Prettier {
216 parser_name,
217 plugin_names,
218 } => Some((parser_name, plugin_names)),
219 })
220 })
221 .fold(
222 HashMap::default(),
223 |mut parsers_with_plugins, (parser_name, plugins)| {
224 match parser_name {
225 Some(parser_name) => parsers_with_plugins
226 .entry(parser_name)
227 .or_insert_with(HashSet::default)
228 .extend(plugins),
229 None => parsers_with_plugins.values_mut().for_each(|existing_plugins| {
230 existing_plugins.extend(plugins.iter());
231 }),
232 }
233 parsers_with_plugins
234 },
235 );
236
237 let selected_parser_with_plugins = parsers_with_plugins.iter().max_by_key(|(_, plugins)| plugins.len());
238 if parsers_with_plugins.len() > 1 {
239 log::warn!("Found multiple parsers with plugins {parsers_with_plugins:?}, will select only one: {selected_parser_with_plugins:?}");
240 }
241
242 let plugin_name_into_path = |plugin_name: &str| self.prettier_dir.join("node_modules").join(plugin_name).join("dist").join("index.mjs");
243 let (parser, plugins) = match selected_parser_with_plugins {
244 Some((parser, plugins)) => {
245 // Tailwind plugin requires being added last
246 // https://github.com/tailwindlabs/prettier-plugin-tailwindcss#compatibility-with-other-prettier-plugins
247 let mut add_tailwind_back = false;
248
249 let mut plugins = plugins.into_iter().filter(|&&plugin_name| {
250 if plugin_name == TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME {
251 add_tailwind_back = true;
252 false
253 } else {
254 true
255 }
256 }).map(|plugin_name| plugin_name_into_path(plugin_name)).collect::<Vec<_>>();
257 if add_tailwind_back {
258 plugins.push(plugin_name_into_path(TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME));
259 }
260 (Some(parser.to_string()), plugins)
261 },
262 None => (None, Vec::new()),
263 };
264
265 let prettier_options = if self.default {
266 let language_settings = language_settings(buffer_language, buffer.file(), cx);
267 let mut options = language_settings.prettier.clone();
268 if !options.contains_key("tabWidth") {
269 options.insert(
270 "tabWidth".to_string(),
271 serde_json::Value::Number(serde_json::Number::from(
272 language_settings.tab_size.get(),
273 )),
274 );
275 }
276 if !options.contains_key("printWidth") {
277 options.insert(
278 "printWidth".to_string(),
279 serde_json::Value::Number(serde_json::Number::from(
280 language_settings.preferred_line_length,
281 )),
282 );
283 }
284 Some(options)
285 } else {
286 None
287 };
288 log::debug!("Formatting file {:?} with prettier, plugins :{plugins:?}, options: {prettier_options:?}", buffer.file().map(|f| f.full_path(cx)));
289
290 FormatParams {
291 text: buffer.text(),
292 options: FormatOptions {
293 parser,
294 plugins,
295 path: buffer_path,
296 prettier_options,
297 },
298 }
299 });
300 let response = self
301 .server
302 .request::<Format>(params)
303 .await
304 .context("prettier format request")?;
305 let diff_task = buffer.read_with(cx, |buffer, cx| buffer.diff(response.text, cx));
306 Ok(diff_task.await)
307 }
308
309 pub async fn clear_cache(&self) -> anyhow::Result<()> {
310 self.server
311 .request::<ClearCache>(())
312 .await
313 .context("prettier clear cache")
314 }
315
316 pub fn server(&self) -> &Arc<LanguageServer> {
317 &self.server
318 }
319
320 pub fn is_default(&self) -> bool {
321 self.default
322 }
323
324 pub fn prettier_dir(&self) -> &Path {
325 &self.prettier_dir
326 }
327
328 pub fn worktree_id(&self) -> Option<usize> {
329 self.worktree_id
330 }
331}
332
333async fn find_closest_prettier_dir(
334 paths_to_check: Vec<PathBuf>,
335 fs: &dyn Fs,
336) -> anyhow::Result<Option<PathBuf>> {
337 for path in paths_to_check {
338 let possible_package_json = path.join("package.json");
339 if let Some(package_json_metadata) = fs
340 .metadata(&possible_package_json)
341 .await
342 .with_context(|| format!("Fetching metadata for {possible_package_json:?}"))?
343 {
344 if !package_json_metadata.is_dir && !package_json_metadata.is_symlink {
345 let package_json_contents = fs
346 .load(&possible_package_json)
347 .await
348 .with_context(|| format!("reading {possible_package_json:?} file contents"))?;
349 if let Ok(json_contents) = serde_json::from_str::<HashMap<String, serde_json::Value>>(
350 &package_json_contents,
351 ) {
352 if let Some(serde_json::Value::Object(o)) = json_contents.get("dependencies") {
353 if o.contains_key(PRETTIER_PACKAGE_NAME) {
354 return Ok(Some(path));
355 }
356 }
357 if let Some(serde_json::Value::Object(o)) = json_contents.get("devDependencies")
358 {
359 if o.contains_key(PRETTIER_PACKAGE_NAME) {
360 return Ok(Some(path));
361 }
362 }
363 }
364 }
365 }
366
367 let possible_node_modules_location = path.join("node_modules").join(PRETTIER_PACKAGE_NAME);
368 if let Some(node_modules_location_metadata) = fs
369 .metadata(&possible_node_modules_location)
370 .await
371 .with_context(|| format!("fetching metadata for {possible_node_modules_location:?}"))?
372 {
373 if node_modules_location_metadata.is_dir {
374 return Ok(Some(path));
375 }
376 }
377 }
378 Ok(None)
379}
380
381enum Format {}
382
383#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
384#[serde(rename_all = "camelCase")]
385struct FormatParams {
386 text: String,
387 options: FormatOptions,
388}
389
390#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
391#[serde(rename_all = "camelCase")]
392struct FormatOptions {
393 plugins: Vec<PathBuf>,
394 parser: Option<String>,
395 #[serde(rename = "filepath")]
396 path: Option<PathBuf>,
397 prettier_options: Option<HashMap<String, serde_json::Value>>,
398}
399
400#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
401#[serde(rename_all = "camelCase")]
402struct FormatResult {
403 text: String,
404}
405
406impl lsp::request::Request for Format {
407 type Params = FormatParams;
408 type Result = FormatResult;
409 const METHOD: &'static str = "prettier/format";
410}
411
412enum ClearCache {}
413
414impl lsp::request::Request for ClearCache {
415 type Params = ();
416 type Result = ();
417 const METHOD: &'static str = "prettier/clear_cache";
418}