1use std::collections::{HashMap, VecDeque};
2use std::path::{Path, PathBuf};
3use std::sync::Arc;
4
5use anyhow::Context;
6use fs::Fs;
7use gpui::{AsyncAppContext, ModelHandle, Task};
8use language::language_settings::language_settings;
9use language::{Buffer, BundledFormatter, Diff};
10use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId};
11use node_runtime::NodeRuntime;
12use serde::{Deserialize, Serialize};
13use util::paths::DEFAULT_PRETTIER_DIR;
14
15pub struct Prettier {
16 server: Arc<LanguageServer>,
17}
18
19#[derive(Debug)]
20pub struct LocateStart {
21 pub worktree_root_path: Arc<Path>,
22 pub starting_path: Arc<Path>,
23}
24
25pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js";
26pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js");
27const PRETTIER_PACKAGE_NAME: &str = "prettier";
28
29impl Prettier {
30 // This was taken from the prettier-vscode extension.
31 pub const CONFIG_FILE_NAMES: &'static [&'static str] = &[
32 ".prettierrc",
33 ".prettierrc.json",
34 ".prettierrc.json5",
35 ".prettierrc.yaml",
36 ".prettierrc.yml",
37 ".prettierrc.toml",
38 ".prettierrc.js",
39 ".prettierrc.cjs",
40 "package.json",
41 "prettier.config.js",
42 "prettier.config.cjs",
43 ".editorconfig",
44 ];
45
46 pub async fn locate(
47 starting_path: Option<LocateStart>,
48 fs: Arc<dyn Fs>,
49 ) -> anyhow::Result<PathBuf> {
50 let paths_to_check = match starting_path.as_ref() {
51 Some(starting_path) => {
52 let worktree_root = starting_path
53 .worktree_root_path
54 .components()
55 .into_iter()
56 .take_while(|path_component| {
57 path_component.as_os_str().to_str() != Some("node_modules")
58 })
59 .collect::<PathBuf>();
60
61 if worktree_root != starting_path.worktree_root_path.as_ref() {
62 vec![worktree_root]
63 } else {
64 let (worktree_root_metadata, start_path_metadata) = if starting_path
65 .starting_path
66 .as_ref()
67 == Path::new("")
68 {
69 let worktree_root_data =
70 fs.metadata(&worktree_root).await.with_context(|| {
71 format!(
72 "FS metadata fetch for worktree root path {worktree_root:?}",
73 )
74 })?;
75 (worktree_root_data.unwrap_or_else(|| {
76 panic!("cannot query prettier for non existing worktree root at {worktree_root_data:?}")
77 }), None)
78 } else {
79 let full_starting_path = worktree_root.join(&starting_path.starting_path);
80 let (worktree_root_data, start_path_data) = futures::try_join!(
81 fs.metadata(&worktree_root),
82 fs.metadata(&full_starting_path),
83 )
84 .with_context(|| {
85 format!("FS metadata fetch for starting path {full_starting_path:?}",)
86 })?;
87 (
88 worktree_root_data.unwrap_or_else(|| {
89 panic!("cannot query prettier for non existing worktree root at {worktree_root_data:?}")
90 }),
91 start_path_data,
92 )
93 };
94
95 match start_path_metadata {
96 Some(start_path_metadata) => {
97 anyhow::ensure!(worktree_root_metadata.is_dir,
98 "For non-empty start path, worktree root {starting_path:?} should be a directory");
99 anyhow::ensure!(
100 !start_path_metadata.is_dir,
101 "For non-empty start path, it should not be a directory {starting_path:?}"
102 );
103 anyhow::ensure!(
104 !start_path_metadata.is_symlink,
105 "For non-empty start path, it should not be a symlink {starting_path:?}"
106 );
107
108 let file_to_format = starting_path.starting_path.as_ref();
109 let mut paths_to_check = VecDeque::from(vec![worktree_root.clone()]);
110 let mut current_path = worktree_root;
111 for path_component in file_to_format.components().into_iter() {
112 current_path = current_path.join(path_component);
113 paths_to_check.push_front(current_path.clone());
114 if path_component.as_os_str().to_str() == Some("node_modules") {
115 break;
116 }
117 }
118 paths_to_check.pop_front(); // last one is the file itself or node_modules, skip it
119 Vec::from(paths_to_check)
120 }
121 None => {
122 anyhow::ensure!(
123 !worktree_root_metadata.is_dir,
124 "For empty start path, worktree root should not be a directory {starting_path:?}"
125 );
126 anyhow::ensure!(
127 !worktree_root_metadata.is_symlink,
128 "For empty start path, worktree root should not be a symlink {starting_path:?}"
129 );
130 worktree_root
131 .parent()
132 .map(|path| vec![path.to_path_buf()])
133 .unwrap_or_default()
134 }
135 }
136 }
137 }
138 None => Vec::new(),
139 };
140
141 match find_closest_prettier_dir(paths_to_check, fs.as_ref())
142 .await
143 .with_context(|| format!("finding prettier starting with {starting_path:?}"))?
144 {
145 Some(prettier_dir) => Ok(prettier_dir),
146 None => Ok(util::paths::DEFAULT_PRETTIER_DIR.to_path_buf()),
147 }
148 }
149
150 pub fn start(
151 prettier_dir: PathBuf,
152 node: Arc<dyn NodeRuntime>,
153 cx: AsyncAppContext,
154 ) -> Task<anyhow::Result<Self>> {
155 cx.spawn(|cx| async move {
156 anyhow::ensure!(
157 prettier_dir.is_dir(),
158 "Prettier dir {prettier_dir:?} is not a directory"
159 );
160 let prettier_server = DEFAULT_PRETTIER_DIR.join(PRETTIER_SERVER_FILE);
161 anyhow::ensure!(
162 prettier_server.is_file(),
163 "no prettier server package found at {prettier_server:?}"
164 );
165
166 let node_path = node.binary_path().await?;
167 let server = LanguageServer::new(
168 LanguageServerId(0),
169 LanguageServerBinary {
170 path: node_path,
171 arguments: vec![prettier_server.into(), prettier_dir.into()],
172 },
173 Path::new("/"),
174 None,
175 cx,
176 )
177 .context("prettier server creation")?;
178 let server = server
179 .initialize(None)
180 .await
181 .context("prettier server initialization")?;
182 Ok(Self { server })
183 })
184 }
185
186 pub async fn format(
187 &self,
188 buffer: &ModelHandle<Buffer>,
189 cx: &AsyncAppContext,
190 ) -> anyhow::Result<Diff> {
191 let params = buffer.read_with(cx, |buffer, cx| {
192 let buffer_file = buffer.file();
193 let buffer_language = buffer.language();
194 let language_settings = language_settings(buffer_language, buffer_file, cx);
195 let path = buffer_file
196 .map(|file| file.full_path(cx))
197 .map(|path| path.to_path_buf());
198 let parser = buffer_language.and_then(|language| {
199 language
200 .lsp_adapters()
201 .iter()
202 .flat_map(|adapter| adapter.enabled_formatters())
203 .find_map(|formatter| match formatter {
204 BundledFormatter::Prettier { parser_name, .. } => {
205 Some(parser_name.to_string())
206 }
207 })
208 });
209 let tab_width = Some(language_settings.tab_size.get());
210 PrettierFormatParams {
211 text: buffer.text(),
212 options: FormatOptions {
213 parser,
214 path,
215 tab_width,
216 },
217 }
218 });
219 let response = self
220 .server
221 .request::<PrettierFormat>(params)
222 .await
223 .context("prettier format request")?;
224 let diff_task = buffer.read_with(cx, |buffer, cx| buffer.diff(response.text, cx));
225 Ok(diff_task.await)
226 }
227
228 pub async fn clear_cache(&self) -> anyhow::Result<()> {
229 todo!()
230 }
231}
232
233async fn find_closest_prettier_dir(
234 paths_to_check: Vec<PathBuf>,
235 fs: &dyn Fs,
236) -> anyhow::Result<Option<PathBuf>> {
237 for path in paths_to_check {
238 let possible_package_json = path.join("package.json");
239 if let Some(package_json_metadata) = fs
240 .metadata(&possible_package_json)
241 .await
242 .with_context(|| format!("Fetching metadata for {possible_package_json:?}"))?
243 {
244 if !package_json_metadata.is_dir && !package_json_metadata.is_symlink {
245 let package_json_contents = fs
246 .load(&possible_package_json)
247 .await
248 .with_context(|| format!("reading {possible_package_json:?} file contents"))?;
249 if let Ok(json_contents) = serde_json::from_str::<HashMap<String, serde_json::Value>>(
250 &package_json_contents,
251 ) {
252 if let Some(serde_json::Value::Object(o)) = json_contents.get("dependencies") {
253 if o.contains_key(PRETTIER_PACKAGE_NAME) {
254 return Ok(Some(path));
255 }
256 }
257 if let Some(serde_json::Value::Object(o)) = json_contents.get("devDependencies")
258 {
259 if o.contains_key(PRETTIER_PACKAGE_NAME) {
260 return Ok(Some(path));
261 }
262 }
263 }
264 }
265 }
266
267 let possible_node_modules_location = path.join("node_modules").join(PRETTIER_PACKAGE_NAME);
268 if let Some(node_modules_location_metadata) = fs
269 .metadata(&possible_node_modules_location)
270 .await
271 .with_context(|| format!("fetching metadata for {possible_node_modules_location:?}"))?
272 {
273 if node_modules_location_metadata.is_dir {
274 return Ok(Some(path));
275 }
276 }
277 }
278 Ok(None)
279}
280
281enum PrettierFormat {}
282
283#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
284#[serde(rename_all = "camelCase")]
285struct PrettierFormatParams {
286 text: String,
287 options: FormatOptions,
288}
289
290#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
291#[serde(rename_all = "camelCase")]
292struct FormatOptions {
293 parser: Option<String>,
294 #[serde(rename = "filepath")]
295 path: Option<PathBuf>,
296 tab_width: Option<u32>,
297}
298
299#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
300#[serde(rename_all = "camelCase")]
301struct PrettierFormatResult {
302 text: String,
303}
304
305impl lsp::request::Request for PrettierFormat {
306 type Params = PrettierFormatParams;
307 type Result = PrettierFormatResult;
308 const METHOD: &'static str = "prettier/format";
309}