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