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        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}