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, Task};
  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 fn start(
151        prettier_dir: PathBuf,
152        node: Arc<dyn NodeRuntime>,
153        cx: AsyncAppContext,
154    ) -> Task<anyhow::Result<Self>> {
155        cx.spawn(|cx| async move {
156            anyhow::ensure!(
157                prettier_dir.is_dir(),
158                "Prettier dir {prettier_dir:?} is not a directory"
159            );
160            let prettier_server = DEFAULT_PRETTIER_DIR.join(PRETTIER_SERVER_FILE);
161            anyhow::ensure!(
162                prettier_server.is_file(),
163                "no prettier server package found at {prettier_server:?}"
164            );
165
166            let node_path = node.binary_path().await?;
167            let server = LanguageServer::new(
168                LanguageServerId(0),
169                LanguageServerBinary {
170                    path: node_path,
171                    arguments: vec![prettier_server.into(), prettier_dir.into()],
172                },
173                Path::new("/"),
174                None,
175                cx,
176            )
177            .context("prettier server creation")?;
178            let server = server
179                .initialize(None)
180                .await
181                .context("prettier server initialization")?;
182            Ok(Self { server })
183        })
184    }
185
186    pub async fn format(
187        &self,
188        buffer: &ModelHandle<Buffer>,
189        cx: &AsyncAppContext,
190    ) -> anyhow::Result<Diff> {
191        let params = buffer.read_with(cx, |buffer, cx| {
192            let buffer_file = buffer.file();
193            let buffer_language = buffer.language();
194            let language_settings = language_settings(buffer_language, buffer_file, cx);
195            let path = buffer_file
196                .map(|file| file.full_path(cx))
197                .map(|path| path.to_path_buf());
198            let parser = buffer_language.and_then(|language| {
199                language
200                    .lsp_adapters()
201                    .iter()
202                    .flat_map(|adapter| adapter.enabled_formatters())
203                    .find_map(|formatter| match formatter {
204                        BundledFormatter::Prettier { parser_name, .. } => {
205                            Some(parser_name.to_string())
206                        }
207                    })
208            });
209            let tab_width = Some(language_settings.tab_size.get());
210            PrettierFormatParams {
211                text: buffer.text(),
212                options: FormatOptions {
213                    parser,
214                    path,
215                    tab_width,
216                },
217            }
218        });
219        let response = self
220            .server
221            .request::<PrettierFormat>(params)
222            .await
223            .context("prettier format request")?;
224        let diff_task = buffer.read_with(cx, |buffer, cx| buffer.diff(response.text, cx));
225        Ok(diff_task.await)
226    }
227
228    pub async fn clear_cache(&self) -> anyhow::Result<()> {
229        todo!()
230    }
231}
232
233async fn find_closest_prettier_dir(
234    paths_to_check: Vec<PathBuf>,
235    fs: &dyn Fs,
236) -> anyhow::Result<Option<PathBuf>> {
237    for path in paths_to_check {
238        let possible_package_json = path.join("package.json");
239        if let Some(package_json_metadata) = fs
240            .metadata(&possible_package_json)
241            .await
242            .with_context(|| format!("Fetching metadata for {possible_package_json:?}"))?
243        {
244            if !package_json_metadata.is_dir && !package_json_metadata.is_symlink {
245                let package_json_contents = fs
246                    .load(&possible_package_json)
247                    .await
248                    .with_context(|| format!("reading {possible_package_json:?} file contents"))?;
249                if let Ok(json_contents) = serde_json::from_str::<HashMap<String, serde_json::Value>>(
250                    &package_json_contents,
251                ) {
252                    if let Some(serde_json::Value::Object(o)) = json_contents.get("dependencies") {
253                        if o.contains_key(PRETTIER_PACKAGE_NAME) {
254                            return Ok(Some(path));
255                        }
256                    }
257                    if let Some(serde_json::Value::Object(o)) = json_contents.get("devDependencies")
258                    {
259                        if o.contains_key(PRETTIER_PACKAGE_NAME) {
260                            return Ok(Some(path));
261                        }
262                    }
263                }
264            }
265        }
266
267        let possible_node_modules_location = path.join("node_modules").join(PRETTIER_PACKAGE_NAME);
268        if let Some(node_modules_location_metadata) = fs
269            .metadata(&possible_node_modules_location)
270            .await
271            .with_context(|| format!("fetching metadata for {possible_node_modules_location:?}"))?
272        {
273            if node_modules_location_metadata.is_dir {
274                return Ok(Some(path));
275            }
276        }
277    }
278    Ok(None)
279}
280
281enum PrettierFormat {}
282
283#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
284#[serde(rename_all = "camelCase")]
285struct PrettierFormatParams {
286    text: String,
287    options: FormatOptions,
288}
289
290#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
291#[serde(rename_all = "camelCase")]
292struct FormatOptions {
293    parser: Option<String>,
294    #[serde(rename = "filepath")]
295    path: Option<PathBuf>,
296    tab_width: Option<u32>,
297}
298
299#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
300#[serde(rename_all = "camelCase")]
301struct PrettierFormatResult {
302    text: String,
303}
304
305impl lsp::request::Request for PrettierFormat {
306    type Params = PrettierFormatParams;
307    type Result = PrettierFormatResult;
308    const METHOD: &'static str = "prettier/format";
309}