prettier.rs

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