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_path(paths_to_check, fs.as_ref())
134            .await
135            .with_context(|| format!("Finding prettier starting with {starting_path:?}"))?
136        {
137            Some(prettier_path) => Ok(prettier_path),
138            None => {
139                // TODO kb return the default prettier, how, without state?
140                Ok(PathBuf::new())
141            }
142        }
143    }
144
145    pub async fn start(prettier_path: &Path, node: Arc<dyn NodeRuntime>) -> anyhow::Result<Self> {
146        todo!()
147    }
148
149    pub async fn format(&self, buffer: &ModelHandle<Buffer>) -> anyhow::Result<Diff> {
150        todo!()
151    }
152
153    pub async fn clear_cache(&self) -> anyhow::Result<()> {
154        todo!()
155    }
156}
157
158const PRETTIER_PACKAGE_NAME: &str = "prettier";
159async fn find_closest_prettier_path(
160    paths_to_check: Vec<PathBuf>,
161    fs: &dyn Fs,
162) -> anyhow::Result<Option<PathBuf>> {
163    for path in paths_to_check {
164        let possible_package_json = path.join("package.json");
165        if let Some(package_json_metadata) = fs
166            .metadata(&path)
167            .await
168            .with_context(|| format!("Fetching metadata for {possible_package_json:?}"))?
169        {
170            if !package_json_metadata.is_dir && !package_json_metadata.is_symlink {
171                let package_json_contents = fs
172                    .load(&possible_package_json)
173                    .await
174                    .with_context(|| format!("reading {possible_package_json:?} file contents"))?;
175                if let Ok(json_contents) = serde_json::from_str::<HashMap<String, serde_json::Value>>(
176                    &package_json_contents,
177                ) {
178                    if let Some(serde_json::Value::Object(o)) = json_contents.get("dependencies") {
179                        if o.contains_key(PRETTIER_PACKAGE_NAME) {
180                            return Ok(Some(path));
181                        }
182                    }
183                    if let Some(serde_json::Value::Object(o)) = json_contents.get("devDependencies")
184                    {
185                        if o.contains_key(PRETTIER_PACKAGE_NAME) {
186                            return Ok(Some(path));
187                        }
188                    }
189                }
190            }
191        }
192
193        let possible_node_modules_location = path.join("node_modules").join(PRETTIER_PACKAGE_NAME);
194        if let Some(node_modules_location_metadata) = fs
195            .metadata(&path)
196            .await
197            .with_context(|| format!("fetching metadata for {possible_node_modules_location:?}"))?
198        {
199            if node_modules_location_metadata.is_dir {
200                return Ok(Some(path));
201            }
202        }
203    }
204    Ok(None)
205}