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