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