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};
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 async fn start(
151 server_id: LanguageServerId,
152 prettier_dir: PathBuf,
153 node: Arc<dyn NodeRuntime>,
154 cx: AsyncAppContext,
155 ) -> anyhow::Result<Self> {
156 let backgroud = cx.background();
157 anyhow::ensure!(
158 prettier_dir.is_dir(),
159 "Prettier dir {prettier_dir:?} is not a directory"
160 );
161 let prettier_server = DEFAULT_PRETTIER_DIR.join(PRETTIER_SERVER_FILE);
162 anyhow::ensure!(
163 prettier_server.is_file(),
164 "no prettier server package found at {prettier_server:?}"
165 );
166
167 let node_path = backgroud
168 .spawn(async move { node.binary_path().await })
169 .await?;
170 let server = LanguageServer::new(
171 server_id,
172 LanguageServerBinary {
173 path: node_path,
174 arguments: vec![prettier_server.into(), prettier_dir.into()],
175 },
176 Path::new("/"),
177 None,
178 cx,
179 )
180 .context("prettier server creation")?;
181 let server = backgroud
182 .spawn(server.initialize(None))
183 .await
184 .context("prettier server initialization")?;
185 Ok(Self { server })
186 }
187
188 pub async fn format(
189 &self,
190 buffer: &ModelHandle<Buffer>,
191 cx: &AsyncAppContext,
192 ) -> anyhow::Result<Diff> {
193 let params = buffer.read_with(cx, |buffer, cx| {
194 let buffer_file = buffer.file();
195 let buffer_language = buffer.language();
196 let language_settings = language_settings(buffer_language, buffer_file, cx);
197 let path = buffer_file
198 .map(|file| file.full_path(cx))
199 .map(|path| path.to_path_buf());
200 let parser = buffer_language.and_then(|language| {
201 language
202 .lsp_adapters()
203 .iter()
204 .flat_map(|adapter| adapter.enabled_formatters())
205 .find_map(|formatter| match formatter {
206 BundledFormatter::Prettier { parser_name, .. } => {
207 Some(parser_name.to_string())
208 }
209 })
210 });
211 let tab_width = Some(language_settings.tab_size.get());
212 FormatParams {
213 text: buffer.text(),
214 options: FormatOptions {
215 parser,
216 path,
217 tab_width,
218 },
219 }
220 });
221 let response = self
222 .server
223 .request::<Format>(params)
224 .await
225 .context("prettier format request")?;
226 let diff_task = buffer.read_with(cx, |buffer, cx| buffer.diff(response.text, cx));
227 Ok(diff_task.await)
228 }
229
230 pub async fn clear_cache(&self) -> anyhow::Result<()> {
231 self.server
232 .request::<ClearCache>(())
233 .await
234 .context("prettier clear cache")
235 }
236
237 pub fn server(&self) -> &Arc<LanguageServer> {
238 &self.server
239 }
240}
241
242async fn find_closest_prettier_dir(
243 paths_to_check: Vec<PathBuf>,
244 fs: &dyn Fs,
245) -> anyhow::Result<Option<PathBuf>> {
246 for path in paths_to_check {
247 let possible_package_json = path.join("package.json");
248 if let Some(package_json_metadata) = fs
249 .metadata(&possible_package_json)
250 .await
251 .with_context(|| format!("Fetching metadata for {possible_package_json:?}"))?
252 {
253 if !package_json_metadata.is_dir && !package_json_metadata.is_symlink {
254 let package_json_contents = fs
255 .load(&possible_package_json)
256 .await
257 .with_context(|| format!("reading {possible_package_json:?} file contents"))?;
258 if let Ok(json_contents) = serde_json::from_str::<HashMap<String, serde_json::Value>>(
259 &package_json_contents,
260 ) {
261 if let Some(serde_json::Value::Object(o)) = json_contents.get("dependencies") {
262 if o.contains_key(PRETTIER_PACKAGE_NAME) {
263 return Ok(Some(path));
264 }
265 }
266 if let Some(serde_json::Value::Object(o)) = json_contents.get("devDependencies")
267 {
268 if o.contains_key(PRETTIER_PACKAGE_NAME) {
269 return Ok(Some(path));
270 }
271 }
272 }
273 }
274 }
275
276 let possible_node_modules_location = path.join("node_modules").join(PRETTIER_PACKAGE_NAME);
277 if let Some(node_modules_location_metadata) = fs
278 .metadata(&possible_node_modules_location)
279 .await
280 .with_context(|| format!("fetching metadata for {possible_node_modules_location:?}"))?
281 {
282 if node_modules_location_metadata.is_dir {
283 return Ok(Some(path));
284 }
285 }
286 }
287 Ok(None)
288}
289
290enum Format {}
291
292#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
293#[serde(rename_all = "camelCase")]
294struct FormatParams {
295 text: String,
296 options: FormatOptions,
297}
298
299#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
300#[serde(rename_all = "camelCase")]
301struct FormatOptions {
302 parser: Option<String>,
303 #[serde(rename = "filepath")]
304 path: Option<PathBuf>,
305 tab_width: Option<u32>,
306}
307
308#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
309#[serde(rename_all = "camelCase")]
310struct FormatResult {
311 text: String,
312}
313
314impl lsp::request::Request for Format {
315 type Params = FormatParams;
316 type Result = FormatResult;
317 const METHOD: &'static str = "prettier/format";
318}
319
320enum ClearCache {}
321
322impl lsp::request::Request for ClearCache {
323 type Params = ();
324 type Result = ();
325 const METHOD: &'static str = "prettier/clear_cache";
326}