1use std::{
2 mem,
3 path::{Path, PathBuf},
4};
5
6use crate::{args::FormatCommand, Taplo};
7use anyhow::anyhow;
8use codespan_reporting::files::SimpleFile;
9
10use taplo::{formatter, parser};
11use taplo_common::{config::Config, environment::Environment, util::Normalize};
12use tokio::io::{AsyncReadExt, AsyncWriteExt};
13
14impl<E: Environment> Taplo<E> {
15 pub async fn execute_format(&mut self, cmd: FormatCommand) -> Result<(), anyhow::Error> {
16 if matches!(cmd.files.first().map(|it| it.as_str()), Some("-")) {
17 self.format_stdin(cmd).await
18 } else {
19 self.format_files(cmd).await
20 }
21 }
22
23 #[tracing::instrument(skip_all)]
24 async fn format_stdin(&mut self, cmd: FormatCommand) -> Result<(), anyhow::Error> {
25 let mut source = String::new();
26 self.env.stdin().read_to_string(&mut source).await?;
27
28 let config = self.load_config(&cmd.general).await?;
29 let display_path = match cmd.stdin_filepath.as_deref() {
30 Some(filepath) if self.env.is_absolute(filepath.as_ref()) => {
31 PathBuf::from(filepath).normalize()
32 }
33 Some(filepath) => {
34 let cwd = self
35 .env
36 .cwd_normalized()
37 .ok_or_else(|| anyhow!("could not figure the current working directory"))?;
38 cwd.join(filepath).normalize()
39 }
40 None => PathBuf::from("-"),
41 };
42 let p = parser::parse(&source);
43
44 if !p.errors.is_empty() {
45 self.print_parse_errors(
46 &SimpleFile::new(&display_path.to_string_lossy(), source.as_str()),
47 &p.errors,
48 )
49 .await?;
50
51 if !cmd.force {
52 return Err(anyhow!("no formatting was done due to syntax errors"));
53 }
54 }
55
56 let format_opts = self.format_options(&config, &cmd, &display_path)?;
57
58 let error_ranges = p.errors.iter().map(|e| e.range).collect::<Vec<_>>();
59
60 let dom = p.into_dom();
61
62 let formatted = formatter::format_with_path_scopes(
63 dom,
64 format_opts,
65 &error_ranges,
66 config.format_scopes(&display_path),
67 )
68 .map_err(|err| anyhow!("invalid key pattern: {err}"))?;
69
70 if cmd.check {
71 if source != formatted {
72 return Err(anyhow!("the input was not properly formatted"));
73 }
74 } else {
75 let mut stdout = self.env.stdout();
76 stdout.write_all(formatted.as_bytes()).await?;
77 stdout.flush().await?;
78 }
79
80 Ok(())
81 }
82
83 #[cfg(target_arch = "wasm32")]
84 async fn print_diff(
85 &self,
86 _path: impl AsRef<Path>,
87 _original: &str,
88 _formatted: &str,
89 ) -> Result<(), anyhow::Error> {
90 tracing::warn!("the `--diff` flag is not available in this build yet");
91 Ok(())
92 }
93
94 #[cfg(not(target_arch = "wasm32"))]
95 async fn print_diff(
96 &self,
97 path: impl AsRef<Path>,
98 original: &str,
99 formatted: &str,
100 ) -> Result<(), anyhow::Error> {
101 let path = path.as_ref();
102 let mut stdout = self.env.stdout();
103
104 macro_rules! echo {
106 ($($args:tt)*) => {
107 let msg = format!("{}\n", std::format_args!($($args)*));
108 stdout.write_all_buf(&mut msg.as_str().as_bytes()).await?;
109 }
110 }
111
112 echo!("diff a/{path} b/{path}", path = path.display());
113 echo!("--- a/{path}", path = path.display());
114 echo!("+++ b/{path}", path = path.display());
115
116 const CONTEXT_LINES: usize = 7;
118
119 let hunks = prettydiff::diff_lines(original, formatted);
120 let hunks = hunks.diff();
121 let hunkcount = hunks.len();
122 let mut acc = Vec::<String>::with_capacity(hunkcount);
123
124 let mut pre_line = 0_usize;
125 let mut post_line = 0_usize;
126
127 for (idx, diff_op) in hunks.into_iter().enumerate() {
128 use ansi_term::Colour::{self, Green, Red};
129 use prettydiff::basic::DiffOp;
130
131 fn apply_color<'a>(
133 s: &'a [&'a str],
134 prefix: &'a str,
135 color: Colour,
136 ) -> impl IntoIterator<Item = String> + 'a {
137 s.iter()
138 .map(move |&s| color.paint(prefix.to_owned() + s).to_string())
139 }
140
141 let mut pre_length = 0_usize;
142 let mut post_length = 0_usize;
143
144 match diff_op {
146 DiffOp::Equal(slices) => {
147 if slices.len() < CONTEXT_LINES * 2 && idx > 0 && idx + 1 < hunkcount {
148 acc.extend(slices[..].iter().map(|&s| s.to_owned()));
149 pre_length += slices.len();
150 post_length += slices.len();
151 } else {
152 if idx > 0 {
153 let end = usize::min(CONTEXT_LINES, slices.len());
154 acc.extend(slices[0..end].iter().map(|&s| s.to_owned()));
155 pre_length += end;
156 post_length += end;
157 }
158 if idx + 1 < hunkcount {
162 let skip = slices.len().saturating_sub(CONTEXT_LINES);
163 acc.extend(slices[skip..].iter().map(|&s| s.to_owned()));
164 let delta = slices.len().saturating_sub(skip);
165 pre_length += delta;
166 post_length += delta;
167 }
168 }
169 }
170 DiffOp::Insert(ins) => {
171 acc.extend(apply_color(ins, "+", Green));
172 post_length += ins.len();
173 }
174 DiffOp::Remove(rem) => {
175 acc.extend(apply_color(rem, "-", Red));
176 pre_length += rem.len();
177 }
178 DiffOp::Replace(rem, ins) => {
179 acc.extend(apply_color(rem, "-", Red));
180 acc.extend(apply_color(ins, "+", Green));
181 pre_length += rem.len();
182 post_length += ins.len();
183 }
184 };
185 echo!(
186 "@@ -{},{} +{},{} @@",
187 pre_line,
188 pre_length,
189 post_line,
190 post_length
191 );
192 echo!("{}", acc.join("\n"));
193
194 pre_line += pre_length;
195 post_line += post_length;
196 acc.clear();
197 }
198
199 stdout.flush().await?;
200 Ok(())
201 }
202
203 #[tracing::instrument(skip_all)]
204 async fn format_files(&mut self, mut cmd: FormatCommand) -> Result<(), anyhow::Error> {
205 if cmd.stdin_filepath.is_some() {
206 tracing::warn!("using `--stdin-filepath` has no effect unless input comes from stdin")
207 }
208
209 let config = self.load_config(&cmd.general).await?;
210
211 let cwd = self
212 .env
213 .cwd_normalized()
214 .ok_or_else(|| anyhow!("could not figure the current working directory"))?;
215
216 let files = self
217 .collect_files(&cwd, &config, mem::take(&mut cmd.files).into_iter())
218 .await?;
219
220 let mut result = Ok(());
221
222 for path in files {
223 let format_opts = self.format_options(&config, &cmd, &path)?;
224
225 let f = self.env.read_file(&path).await?;
226 let source = String::from_utf8_lossy(&f).into_owned();
227
228 let p = parser::parse(&source);
229
230 if !p.errors.is_empty() {
231 self.print_parse_errors(
232 &SimpleFile::new(&*path.to_string_lossy(), source.as_str()),
233 &p.errors,
234 )
235 .await?;
236
237 if !cmd.force {
238 result = Err(anyhow!(
239 "some files were not formatted due to syntax errors"
240 ));
241 continue;
242 }
243 }
244
245 let error_ranges = p.errors.iter().map(|e| e.range).collect::<Vec<_>>();
246
247 let dom = p.into_dom();
248
249 let formatted = formatter::format_with_path_scopes(
250 dom,
251 format_opts,
252 &error_ranges,
253 config.format_scopes(&path),
254 )
255 .map_err(|err| anyhow!("invalid key pattern: {err}"))?;
256
257 if source != formatted {
258 if cmd.diff {
259 if let Err(e) = self.print_diff(&path, &source, &formatted).await {
260 self.env
261 .stderr()
262 .write_all(
263 format!("Failed to write diff to stdout: {:?}", e)
264 .as_str()
265 .as_bytes(),
266 )
267 .await?;
268 }
269 }
270
271 if cmd.check {
272 tracing::error!(?path, "the file is not properly formatted");
273 result = Err(anyhow!("some files were not properly formatted"));
274 } else {
275 self.env.write_file(&path, formatted.as_bytes()).await?;
276 }
277 }
278 }
279
280 result
281 }
282
283 fn format_options(
284 &self,
285 config: &Config,
286 cmd: &FormatCommand,
287 path: &Path,
288 ) -> Result<formatter::Options, anyhow::Error> {
289 let mut format_opts = formatter::Options::default();
290 config.update_format_options(path, &mut format_opts);
291
292 format_opts.update_from_str(cmd.options.iter().filter_map(|s| {
293 let mut split = s.split('=');
294 let k = split.next();
295 let v = split.next();
296
297 if let (Some(k), Some(v)) = (k, v) {
298 Some((k, v))
299 } else {
300 tracing::error!(option = %s, "malformed formatter option");
301 None
302 }
303 }))?;
304
305 Ok(format_opts)
306 }
307}