prettier.rs

  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}