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