prettier.rs

  1use std::collections::{HashMap, VecDeque};
  2use std::path::{Path, PathBuf};
  3use std::sync::Arc;
  4
  5use anyhow::Context;
  6use fs::Fs;
  7use gpui::{AsyncAppContext, ModelHandle};
  8use language::language_settings::language_settings;
  9use language::{Buffer, BundledFormatter, Diff};
 10use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId};
 11use node_runtime::NodeRuntime;
 12use serde::{Deserialize, Serialize};
 13use util::paths::DEFAULT_PRETTIER_DIR;
 14
 15pub struct Prettier {
 16    server: Arc<LanguageServer>,
 17}
 18
 19#[derive(Debug)]
 20pub struct LocateStart {
 21    pub worktree_root_path: Arc<Path>,
 22    pub starting_path: Arc<Path>,
 23}
 24
 25pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js";
 26pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js");
 27const PRETTIER_PACKAGE_NAME: &str = "prettier";
 28
 29impl Prettier {
 30    // This was taken from the prettier-vscode extension.
 31    pub const CONFIG_FILE_NAMES: &'static [&'static str] = &[
 32        ".prettierrc",
 33        ".prettierrc.json",
 34        ".prettierrc.json5",
 35        ".prettierrc.yaml",
 36        ".prettierrc.yml",
 37        ".prettierrc.toml",
 38        ".prettierrc.js",
 39        ".prettierrc.cjs",
 40        "package.json",
 41        "prettier.config.js",
 42        "prettier.config.cjs",
 43        ".editorconfig",
 44    ];
 45
 46    pub async fn locate(
 47        starting_path: Option<LocateStart>,
 48        fs: Arc<dyn Fs>,
 49    ) -> anyhow::Result<PathBuf> {
 50        let paths_to_check = match starting_path.as_ref() {
 51            Some(starting_path) => {
 52                let worktree_root = starting_path
 53                    .worktree_root_path
 54                    .components()
 55                    .into_iter()
 56                    .take_while(|path_component| {
 57                        path_component.as_os_str().to_str() != Some("node_modules")
 58                    })
 59                    .collect::<PathBuf>();
 60
 61                if worktree_root != starting_path.worktree_root_path.as_ref() {
 62                    vec![worktree_root]
 63                } else {
 64                    let (worktree_root_metadata, start_path_metadata) = if starting_path
 65                        .starting_path
 66                        .as_ref()
 67                        == Path::new("")
 68                    {
 69                        let worktree_root_data =
 70                            fs.metadata(&worktree_root).await.with_context(|| {
 71                                format!(
 72                                    "FS metadata fetch for worktree root path {worktree_root:?}",
 73                                )
 74                            })?;
 75                        (worktree_root_data.unwrap_or_else(|| {
 76                            panic!("cannot query prettier for non existing worktree root at {worktree_root_data:?}")
 77                        }), None)
 78                    } else {
 79                        let full_starting_path = worktree_root.join(&starting_path.starting_path);
 80                        let (worktree_root_data, start_path_data) = futures::try_join!(
 81                            fs.metadata(&worktree_root),
 82                            fs.metadata(&full_starting_path),
 83                        )
 84                        .with_context(|| {
 85                            format!("FS metadata fetch for starting path {full_starting_path:?}",)
 86                        })?;
 87                        (
 88                            worktree_root_data.unwrap_or_else(|| {
 89                                panic!("cannot query prettier for non existing worktree root at {worktree_root_data:?}")
 90                            }),
 91                            start_path_data,
 92                        )
 93                    };
 94
 95                    match start_path_metadata {
 96                        Some(start_path_metadata) => {
 97                            anyhow::ensure!(worktree_root_metadata.is_dir,
 98                                "For non-empty start path, worktree root {starting_path:?} should be a directory");
 99                            anyhow::ensure!(
100                                !start_path_metadata.is_dir,
101                                "For non-empty start path, it should not be a directory {starting_path:?}"
102                            );
103                            anyhow::ensure!(
104                                !start_path_metadata.is_symlink,
105                                "For non-empty start path, it should not be a symlink {starting_path:?}"
106                            );
107
108                            let file_to_format = starting_path.starting_path.as_ref();
109                            let mut paths_to_check = VecDeque::from(vec![worktree_root.clone()]);
110                            let mut current_path = worktree_root;
111                            for path_component in file_to_format.components().into_iter() {
112                                current_path = current_path.join(path_component);
113                                paths_to_check.push_front(current_path.clone());
114                                if path_component.as_os_str().to_str() == Some("node_modules") {
115                                    break;
116                                }
117                            }
118                            paths_to_check.pop_front(); // last one is the file itself or node_modules, skip it
119                            Vec::from(paths_to_check)
120                        }
121                        None => {
122                            anyhow::ensure!(
123                                !worktree_root_metadata.is_dir,
124                                "For empty start path, worktree root should not be a directory {starting_path:?}"
125                            );
126                            anyhow::ensure!(
127                                !worktree_root_metadata.is_symlink,
128                                "For empty start path, worktree root should not be a symlink {starting_path:?}"
129                            );
130                            worktree_root
131                                .parent()
132                                .map(|path| vec![path.to_path_buf()])
133                                .unwrap_or_default()
134                        }
135                    }
136                }
137            }
138            None => Vec::new(),
139        };
140
141        match find_closest_prettier_dir(paths_to_check, fs.as_ref())
142            .await
143            .with_context(|| format!("finding prettier starting with {starting_path:?}"))?
144        {
145            Some(prettier_dir) => Ok(prettier_dir),
146            None => Ok(util::paths::DEFAULT_PRETTIER_DIR.to_path_buf()),
147        }
148    }
149
150    pub async fn start(
151        server_id: LanguageServerId,
152        prettier_dir: PathBuf,
153        node: Arc<dyn NodeRuntime>,
154        cx: AsyncAppContext,
155    ) -> anyhow::Result<Self> {
156        let backgroud = cx.background();
157        anyhow::ensure!(
158            prettier_dir.is_dir(),
159            "Prettier dir {prettier_dir:?} is not a directory"
160        );
161        let prettier_server = DEFAULT_PRETTIER_DIR.join(PRETTIER_SERVER_FILE);
162        anyhow::ensure!(
163            prettier_server.is_file(),
164            "no prettier server package found at {prettier_server:?}"
165        );
166
167        let node_path = backgroud
168            .spawn(async move { node.binary_path().await })
169            .await?;
170        let server = LanguageServer::new(
171            server_id,
172            LanguageServerBinary {
173                path: node_path,
174                arguments: vec![prettier_server.into(), prettier_dir.into()],
175            },
176            Path::new("/"),
177            None,
178            cx,
179        )
180        .context("prettier server creation")?;
181        let server = backgroud
182            .spawn(server.initialize(None))
183            .await
184            .context("prettier server initialization")?;
185        Ok(Self { server })
186    }
187
188    pub async fn format(
189        &self,
190        buffer: &ModelHandle<Buffer>,
191        cx: &AsyncAppContext,
192    ) -> anyhow::Result<Diff> {
193        let params = buffer.read_with(cx, |buffer, cx| {
194            let buffer_file = buffer.file();
195            let buffer_language = buffer.language();
196            let language_settings = language_settings(buffer_language, buffer_file, cx);
197            let path = buffer_file
198                .map(|file| file.full_path(cx))
199                .map(|path| path.to_path_buf());
200            let parser = buffer_language.and_then(|language| {
201                language
202                    .lsp_adapters()
203                    .iter()
204                    .flat_map(|adapter| adapter.enabled_formatters())
205                    .find_map(|formatter| match formatter {
206                        BundledFormatter::Prettier { parser_name, .. } => {
207                            Some(parser_name.to_string())
208                        }
209                    })
210            });
211            let tab_width = Some(language_settings.tab_size.get());
212            FormatParams {
213                text: buffer.text(),
214                options: FormatOptions {
215                    parser,
216                    path,
217                    tab_width,
218                },
219            }
220        });
221        let response = self
222            .server
223            .request::<Format>(params)
224            .await
225            .context("prettier format request")?;
226        let diff_task = buffer.read_with(cx, |buffer, cx| buffer.diff(response.text, cx));
227        Ok(diff_task.await)
228    }
229
230    pub async fn clear_cache(&self) -> anyhow::Result<()> {
231        self.server
232            .request::<ClearCache>(())
233            .await
234            .context("prettier clear cache")
235    }
236
237    pub fn server(&self) -> &Arc<LanguageServer> {
238        &self.server
239    }
240}
241
242async fn find_closest_prettier_dir(
243    paths_to_check: Vec<PathBuf>,
244    fs: &dyn Fs,
245) -> anyhow::Result<Option<PathBuf>> {
246    for path in paths_to_check {
247        let possible_package_json = path.join("package.json");
248        if let Some(package_json_metadata) = fs
249            .metadata(&possible_package_json)
250            .await
251            .with_context(|| format!("Fetching metadata for {possible_package_json:?}"))?
252        {
253            if !package_json_metadata.is_dir && !package_json_metadata.is_symlink {
254                let package_json_contents = fs
255                    .load(&possible_package_json)
256                    .await
257                    .with_context(|| format!("reading {possible_package_json:?} file contents"))?;
258                if let Ok(json_contents) = serde_json::from_str::<HashMap<String, serde_json::Value>>(
259                    &package_json_contents,
260                ) {
261                    if let Some(serde_json::Value::Object(o)) = json_contents.get("dependencies") {
262                        if o.contains_key(PRETTIER_PACKAGE_NAME) {
263                            return Ok(Some(path));
264                        }
265                    }
266                    if let Some(serde_json::Value::Object(o)) = json_contents.get("devDependencies")
267                    {
268                        if o.contains_key(PRETTIER_PACKAGE_NAME) {
269                            return Ok(Some(path));
270                        }
271                    }
272                }
273            }
274        }
275
276        let possible_node_modules_location = path.join("node_modules").join(PRETTIER_PACKAGE_NAME);
277        if let Some(node_modules_location_metadata) = fs
278            .metadata(&possible_node_modules_location)
279            .await
280            .with_context(|| format!("fetching metadata for {possible_node_modules_location:?}"))?
281        {
282            if node_modules_location_metadata.is_dir {
283                return Ok(Some(path));
284            }
285        }
286    }
287    Ok(None)
288}
289
290enum Format {}
291
292#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
293#[serde(rename_all = "camelCase")]
294struct FormatParams {
295    text: String,
296    options: FormatOptions,
297}
298
299#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
300#[serde(rename_all = "camelCase")]
301struct FormatOptions {
302    parser: Option<String>,
303    #[serde(rename = "filepath")]
304    path: Option<PathBuf>,
305    tab_width: Option<u32>,
306}
307
308#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
309#[serde(rename_all = "camelCase")]
310struct FormatResult {
311    text: String,
312}
313
314impl lsp::request::Request for Format {
315    type Params = FormatParams;
316    type Result = FormatResult;
317    const METHOD: &'static str = "prettier/format";
318}
319
320enum ClearCache {}
321
322impl lsp::request::Request for ClearCache {
323    type Params = ();
324    type Result = ();
325    const METHOD: &'static str = "prettier/clear_cache";
326}