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::ModelHandle;
  8use language::{Buffer, Diff};
  9use node_runtime::NodeRuntime;
 10
 11pub struct Prettier {
 12    _private: (),
 13}
 14
 15#[derive(Debug)]
 16pub struct LocateStart {
 17    pub worktree_root_path: Arc<Path>,
 18    pub starting_path: Arc<Path>,
 19}
 20
 21pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js");
 22
 23impl Prettier {
 24    // This was taken from the prettier-vscode extension.
 25    pub const CONFIG_FILE_NAMES: &'static [&'static str] = &[
 26        ".prettierrc",
 27        ".prettierrc.json",
 28        ".prettierrc.json5",
 29        ".prettierrc.yaml",
 30        ".prettierrc.yml",
 31        ".prettierrc.toml",
 32        ".prettierrc.js",
 33        ".prettierrc.cjs",
 34        "package.json",
 35        "prettier.config.js",
 36        "prettier.config.cjs",
 37        ".editorconfig",
 38    ];
 39
 40    pub async fn locate(
 41        starting_path: Option<LocateStart>,
 42        fs: Arc<dyn Fs>,
 43    ) -> anyhow::Result<PathBuf> {
 44        let paths_to_check = match starting_path.as_ref() {
 45            Some(starting_path) => {
 46                let worktree_root = starting_path
 47                    .worktree_root_path
 48                    .components()
 49                    .into_iter()
 50                    .take_while(|path_component| {
 51                        path_component.as_os_str().to_str() != Some("node_modules")
 52                    })
 53                    .collect::<PathBuf>();
 54
 55                if worktree_root != starting_path.worktree_root_path.as_ref() {
 56                    vec![worktree_root]
 57                } else {
 58                    let (worktree_root_metadata, start_path_metadata) = if starting_path
 59                        .starting_path
 60                        .as_ref()
 61                        == Path::new("")
 62                    {
 63                        let worktree_root_data =
 64                            fs.metadata(&worktree_root).await.with_context(|| {
 65                                format!(
 66                                    "FS metadata fetch for worktree root path {worktree_root:?}",
 67                                )
 68                            })?;
 69                        (worktree_root_data.unwrap_or_else(|| {
 70                            panic!("cannot query prettier for non existing worktree root at {worktree_root_data:?}")
 71                        }), None)
 72                    } else {
 73                        let full_starting_path = worktree_root.join(&starting_path.starting_path);
 74                        let (worktree_root_data, start_path_data) = futures::try_join!(
 75                            fs.metadata(&worktree_root),
 76                            fs.metadata(&full_starting_path),
 77                        )
 78                        .with_context(|| {
 79                            format!("FS metadata fetch for starting path {full_starting_path:?}",)
 80                        })?;
 81                        (
 82                            worktree_root_data.unwrap_or_else(|| {
 83                                panic!("cannot query prettier for non existing worktree root at {worktree_root_data:?}")
 84                            }),
 85                            start_path_data,
 86                        )
 87                    };
 88
 89                    match start_path_metadata {
 90                        Some(start_path_metadata) => {
 91                            anyhow::ensure!(worktree_root_metadata.is_dir,
 92                                "For non-empty start path, worktree root {starting_path:?} should be a directory");
 93                            anyhow::ensure!(
 94                                !start_path_metadata.is_dir,
 95                                "For non-empty start path, it should not be a directory {starting_path:?}"
 96                            );
 97                            anyhow::ensure!(
 98                                !start_path_metadata.is_symlink,
 99                                "For non-empty start path, it should not be a symlink {starting_path:?}"
100                            );
101
102                            let file_to_format = starting_path.starting_path.as_ref();
103                            let mut paths_to_check = VecDeque::from(vec![worktree_root.clone()]);
104                            let mut current_path = worktree_root;
105                            for path_component in file_to_format.components().into_iter() {
106                                current_path = current_path.join(path_component);
107                                paths_to_check.push_front(current_path.clone());
108                                if path_component.as_os_str().to_str() == Some("node_modules") {
109                                    break;
110                                }
111                            }
112                            paths_to_check.pop_front(); // last one is the file itself or node_modules, skip it
113                            Vec::from(paths_to_check)
114                        }
115                        None => {
116                            anyhow::ensure!(
117                                !worktree_root_metadata.is_dir,
118                                "For empty start path, worktree root should not be a directory {starting_path:?}"
119                            );
120                            anyhow::ensure!(
121                                !worktree_root_metadata.is_symlink,
122                                "For empty start path, worktree root should not be a symlink {starting_path:?}"
123                            );
124                            worktree_root
125                                .parent()
126                                .map(|path| vec![path.to_path_buf()])
127                                .unwrap_or_default()
128                        }
129                    }
130                }
131            }
132            None => Vec::new(),
133        };
134
135        match find_closest_prettier_dir(paths_to_check, fs.as_ref())
136            .await
137            .with_context(|| format!("finding prettier starting with {starting_path:?}"))?
138        {
139            Some(prettier_dir) => Ok(prettier_dir),
140            None => Ok(util::paths::DEFAULT_PRETTIER_DIR.to_path_buf()),
141        }
142    }
143
144    pub async fn start(prettier_dir: &Path, node: Arc<dyn NodeRuntime>) -> anyhow::Result<Self> {
145        anyhow::ensure!(
146            prettier_dir.is_dir(),
147            "Prettier dir {prettier_dir:?} is not a directory"
148        );
149        anyhow::bail!("TODO kb: start prettier server in {prettier_dir:?}")
150    }
151
152    pub async fn format(&self, buffer: &ModelHandle<Buffer>) -> anyhow::Result<Diff> {
153        todo!()
154    }
155
156    pub async fn clear_cache(&self) -> anyhow::Result<()> {
157        todo!()
158    }
159}
160
161const PRETTIER_PACKAGE_NAME: &str = "prettier";
162async fn find_closest_prettier_dir(
163    paths_to_check: Vec<PathBuf>,
164    fs: &dyn Fs,
165) -> anyhow::Result<Option<PathBuf>> {
166    for path in paths_to_check {
167        let possible_package_json = path.join("package.json");
168        if let Some(package_json_metadata) = fs
169            .metadata(&possible_package_json)
170            .await
171            .with_context(|| format!("Fetching metadata for {possible_package_json:?}"))?
172        {
173            if !package_json_metadata.is_dir && !package_json_metadata.is_symlink {
174                let package_json_contents = fs
175                    .load(&possible_package_json)
176                    .await
177                    .with_context(|| format!("reading {possible_package_json:?} file contents"))?;
178                if let Ok(json_contents) = serde_json::from_str::<HashMap<String, serde_json::Value>>(
179                    &package_json_contents,
180                ) {
181                    if let Some(serde_json::Value::Object(o)) = json_contents.get("dependencies") {
182                        if o.contains_key(PRETTIER_PACKAGE_NAME) {
183                            return Ok(Some(path));
184                        }
185                    }
186                    if let Some(serde_json::Value::Object(o)) = json_contents.get("devDependencies")
187                    {
188                        if o.contains_key(PRETTIER_PACKAGE_NAME) {
189                            return Ok(Some(path));
190                        }
191                    }
192                }
193            }
194        }
195
196        let possible_node_modules_location = path.join("node_modules").join(PRETTIER_PACKAGE_NAME);
197        if let Some(node_modules_location_metadata) = fs
198            .metadata(&possible_node_modules_location)
199            .await
200            .with_context(|| format!("fetching metadata for {possible_node_modules_location:?}"))?
201        {
202            if node_modules_location_metadata.is_dir {
203                return Ok(Some(path));
204            }
205        }
206    }
207    Ok(None)
208}