prettier2.rs

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