prettier.rs

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