taplo_cli/commands/
format.rs

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        // print to stdout
105        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        // How many lines of context to print:
117        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            // apply the given color and prefix to the set of strings `s`
132            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            // length of a net diff op
145            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                        // context before the hunk within the file
159
160                        // context after the hunk within the file
161                        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}