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