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