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}