prettier2.rs

  1use anyhow::Context;
  2use collections::HashMap;
  3use fs2::Fs;
  4use gpui2::{AsyncAppContext, Model};
  5use language2::{language_settings::language_settings, Buffer, 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
193                    .update(cx, |buffer, cx| {
194                        let buffer_language = buffer.language();
195                        let parser_with_plugins = buffer_language.and_then(|l| {
196                            let prettier_parser = l.prettier_parser_name()?;
197                            let mut prettier_plugins = l
198                                .lsp_adapters()
199                                .iter()
200                                .flat_map(|adapter| adapter.prettier_plugins())
201                                .collect::<Vec<_>>();
202                            prettier_plugins.dedup();
203                            Some((prettier_parser, prettier_plugins))
204                        });
205
206                        let prettier_node_modules = self.prettier_dir().join("node_modules");
207                        anyhow::ensure!(
208                            prettier_node_modules.is_dir(),
209                            "Prettier node_modules dir does not exist: {prettier_node_modules:?}"
210                        );
211                        let plugin_name_into_path = |plugin_name: &str| {
212                            let prettier_plugin_dir = prettier_node_modules.join(plugin_name);
213                            for possible_plugin_path in [
214                                prettier_plugin_dir.join("dist").join("index.mjs"),
215                                prettier_plugin_dir.join("dist").join("index.js"),
216                                prettier_plugin_dir.join("dist").join("plugin.js"),
217                                prettier_plugin_dir.join("index.mjs"),
218                                prettier_plugin_dir.join("index.js"),
219                                prettier_plugin_dir.join("plugin.js"),
220                                prettier_plugin_dir,
221                            ] {
222                                if possible_plugin_path.is_file() {
223                                    return Some(possible_plugin_path);
224                                }
225                            }
226                            None
227                        };
228                        let (parser, located_plugins) = match parser_with_plugins {
229                            Some((parser, plugins)) => {
230                                // Tailwind plugin requires being added last
231                                // https://github.com/tailwindlabs/prettier-plugin-tailwindcss#compatibility-with-other-prettier-plugins
232                                let mut add_tailwind_back = false;
233
234                                let mut plugins = plugins
235                                    .into_iter()
236                                    .filter(|&&plugin_name| {
237                                        if plugin_name == TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME {
238                                            add_tailwind_back = true;
239                                            false
240                                        } else {
241                                            true
242                                        }
243                                    })
244                                    .map(|plugin_name| {
245                                        (plugin_name, plugin_name_into_path(plugin_name))
246                                    })
247                                    .collect::<Vec<_>>();
248                                if add_tailwind_back {
249                                    plugins.push((
250                                        &TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME,
251                                        plugin_name_into_path(
252                                            TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME,
253                                        ),
254                                    ));
255                                }
256                                (Some(parser.to_string()), plugins)
257                            }
258                            None => (None, Vec::new()),
259                        };
260
261                        let prettier_options = if self.is_default() {
262                            let language_settings =
263                                language_settings(buffer_language, buffer.file(), cx);
264                            let mut options = language_settings.prettier.clone();
265                            if !options.contains_key("tabWidth") {
266                                options.insert(
267                                    "tabWidth".to_string(),
268                                    serde_json::Value::Number(serde_json::Number::from(
269                                        language_settings.tab_size.get(),
270                                    )),
271                                );
272                            }
273                            if !options.contains_key("printWidth") {
274                                options.insert(
275                                    "printWidth".to_string(),
276                                    serde_json::Value::Number(serde_json::Number::from(
277                                        language_settings.preferred_line_length,
278                                    )),
279                                );
280                            }
281                            Some(options)
282                        } else {
283                            None
284                        };
285
286                        let plugins = located_plugins
287                            .into_iter()
288                            .filter_map(|(plugin_name, located_plugin_path)| {
289                                match located_plugin_path {
290                                    Some(path) => Some(path),
291                                    None => {
292                                        log::error!(
293                                            "Have not found plugin path for {:?} inside {:?}",
294                                            plugin_name,
295                                            prettier_node_modules
296                                        );
297                                        None
298                                    }
299                                }
300                            })
301                            .collect();
302                        log::debug!(
303                            "Formatting file {:?} with prettier, plugins :{:?}, options: {:?}",
304                            plugins,
305                            prettier_options,
306                            buffer.file().map(|f| f.full_path(cx))
307                        );
308
309                        anyhow::Ok(FormatParams {
310                            text: buffer.text(),
311                            options: FormatOptions {
312                                parser,
313                                plugins,
314                                path: buffer_path,
315                                prettier_options,
316                            },
317                        })
318                    })?
319                    .context("prettier params calculation")?;
320                let response = local
321                    .server
322                    .request::<Format>(params)
323                    .await
324                    .context("prettier format request")?;
325                let diff_task = buffer.update(cx, |buffer, cx| buffer.diff(response.text, cx))?;
326                Ok(diff_task.await)
327            }
328            #[cfg(any(test, feature = "test-support"))]
329            Self::Test(_) => Ok(buffer
330                .update(cx, |buffer, cx| {
331                    let formatted_text = buffer.text() + Self::FORMAT_SUFFIX;
332                    buffer.diff(formatted_text, cx)
333                })?
334                .await),
335        }
336    }
337
338    pub async fn clear_cache(&self) -> anyhow::Result<()> {
339        match self {
340            Self::Real(local) => local
341                .server
342                .request::<ClearCache>(())
343                .await
344                .context("prettier clear cache"),
345            #[cfg(any(test, feature = "test-support"))]
346            Self::Test(_) => Ok(()),
347        }
348    }
349
350    pub fn server(&self) -> Option<&Arc<LanguageServer>> {
351        match self {
352            Self::Real(local) => Some(&local.server),
353            #[cfg(any(test, feature = "test-support"))]
354            Self::Test(_) => None,
355        }
356    }
357
358    pub fn is_default(&self) -> bool {
359        match self {
360            Self::Real(local) => local.default,
361            #[cfg(any(test, feature = "test-support"))]
362            Self::Test(test_prettier) => test_prettier.default,
363        }
364    }
365
366    pub fn prettier_dir(&self) -> &Path {
367        match self {
368            Self::Real(local) => &local.prettier_dir,
369            #[cfg(any(test, feature = "test-support"))]
370            Self::Test(test_prettier) => &test_prettier.prettier_dir,
371        }
372    }
373
374    pub fn worktree_id(&self) -> Option<usize> {
375        match self {
376            Self::Real(local) => local.worktree_id,
377            #[cfg(any(test, feature = "test-support"))]
378            Self::Test(test_prettier) => test_prettier.worktree_id,
379        }
380    }
381}
382
383async fn find_closest_prettier_dir(
384    paths_to_check: Vec<PathBuf>,
385    fs: &dyn Fs,
386) -> anyhow::Result<Option<PathBuf>> {
387    for path in paths_to_check {
388        let possible_package_json = path.join("package.json");
389        if let Some(package_json_metadata) = fs
390            .metadata(&possible_package_json)
391            .await
392            .with_context(|| format!("Fetching metadata for {possible_package_json:?}"))?
393        {
394            if !package_json_metadata.is_dir && !package_json_metadata.is_symlink {
395                let package_json_contents = fs
396                    .load(&possible_package_json)
397                    .await
398                    .with_context(|| format!("reading {possible_package_json:?} file contents"))?;
399                if let Ok(json_contents) = serde_json::from_str::<HashMap<String, serde_json::Value>>(
400                    &package_json_contents,
401                ) {
402                    if let Some(serde_json::Value::Object(o)) = json_contents.get("dependencies") {
403                        if o.contains_key(PRETTIER_PACKAGE_NAME) {
404                            return Ok(Some(path));
405                        }
406                    }
407                    if let Some(serde_json::Value::Object(o)) = json_contents.get("devDependencies")
408                    {
409                        if o.contains_key(PRETTIER_PACKAGE_NAME) {
410                            return Ok(Some(path));
411                        }
412                    }
413                }
414            }
415        }
416
417        let possible_node_modules_location = path.join("node_modules").join(PRETTIER_PACKAGE_NAME);
418        if let Some(node_modules_location_metadata) = fs
419            .metadata(&possible_node_modules_location)
420            .await
421            .with_context(|| format!("fetching metadata for {possible_node_modules_location:?}"))?
422        {
423            if node_modules_location_metadata.is_dir {
424                return Ok(Some(path));
425            }
426        }
427    }
428    Ok(None)
429}
430
431enum Format {}
432
433#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
434#[serde(rename_all = "camelCase")]
435struct FormatParams {
436    text: String,
437    options: FormatOptions,
438}
439
440#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
441#[serde(rename_all = "camelCase")]
442struct FormatOptions {
443    plugins: Vec<PathBuf>,
444    parser: Option<String>,
445    #[serde(rename = "filepath")]
446    path: Option<PathBuf>,
447    prettier_options: Option<HashMap<String, serde_json::Value>>,
448}
449
450#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
451#[serde(rename_all = "camelCase")]
452struct FormatResult {
453    text: String,
454}
455
456impl lsp2::request::Request for Format {
457    type Params = FormatParams;
458    type Result = FormatResult;
459    const METHOD: &'static str = "prettier/format";
460}
461
462enum ClearCache {}
463
464impl lsp2::request::Request for ClearCache {
465    type Params = ();
466    type Result = ();
467    const METHOD: &'static str = "prettier/clear_cache";
468}