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}