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