Skip to content

Commit a69e9d4

Browse files
authored
Implement stdin support (#471)
* Implement stdin support * `&&` not `&` * Missing `,` * Clean up style * Add empty stdin test * Add more relevant permissions * Fix misleading test case * Update snapshots for shell completion
1 parent dc8b26c commit a69e9d4

32 files changed

Lines changed: 1090 additions & 290 deletions

.claude/settings.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,24 @@
33
"permissions": {
44
"defaultMode": "acceptEdits",
55
"allow": [
6+
"Bash(cargo insta accept:*)",
67
"Bash(cat:*)",
78
"Bash(find:*)",
89
"Bash(gh issue list:*)",
910
"Bash(gh issue view:*)",
1011
"Bash(gh pr diff:*)",
1112
"Bash(gh pr view:*)",
1213
"Bash(git checkout:*)",
14+
"Bash(git diff:*)",
1315
"Bash(git grep:*)",
16+
"Bash(git log:*)",
1417
"Bash(grep:*)",
18+
"Bash(just test:*)",
1519
"Bash(ls:*)",
1620
"Bash(rm:*)",
1721
"Bash(sed:*)",
18-
"WebFetch(domain:github.com)",
19-
"WebFetch(domain:raw.githubusercontent.com)"
22+
"WebFetch",
23+
"WebSearch"
2024
]
2125
}
2226
}

crates/air/src/args.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,15 @@ pub(crate) struct FormatCommand {
5454
/// and zero otherwise.
5555
#[arg(long)]
5656
pub check: bool,
57+
58+
/// Use this option to enable reading from stdin and writing to stdout. This specifies
59+
/// a file path to associate the standard input with, which is used as the location to
60+
/// begin searching for configuration files from. The file does not have to exist and
61+
/// will not be read from. If a relative path is provided, it is resolved from the
62+
/// current working directory. If this option is specified, no other files or
63+
/// directories can be provided.
64+
#[arg(long)]
65+
pub stdin_file_path: Option<PathBuf>,
5766
}
5867

5968
#[derive(Clone, Debug, Parser)]

crates/air/src/commands/format.rs

Lines changed: 25 additions & 202 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,43 @@
1-
use std::fmt::Display;
2-
use std::fmt::Formatter;
3-
use std::io;
4-
use std::io::Write;
5-
use std::io::stderr;
6-
use std::path::Path;
7-
use std::path::PathBuf;
8-
9-
use colored::Colorize;
10-
use fs::relativize_path;
11-
use itertools::Either;
12-
use itertools::Itertools;
13-
use thiserror::Error;
14-
use workspace::discovery::DiscoveredSettings;
15-
use workspace::discovery::discover_r_file_paths;
16-
use workspace::discovery::discover_settings;
17-
use workspace::format::FormatFileError;
18-
use workspace::format::FormattedFile;
19-
use workspace::format::format_file;
20-
use workspace::resolve::PathResolver;
21-
use workspace::settings::FormatSettings;
22-
use workspace::settings::Settings;
23-
241
use crate::ExitStatus;
252
use crate::args::FormatCommand;
263

4+
mod paths;
5+
mod stdin;
6+
277
#[derive(Copy, Clone, Debug)]
288
enum FormatMode {
299
Write,
3010
Check,
3111
}
3212

33-
#[derive(Error, Debug)]
34-
enum FormatPathError {
35-
FormatFile(PathBuf, FormatFileError),
36-
Write(PathBuf, io::Error),
37-
Ignore(#[from] ignore::Error),
38-
}
39-
4013
pub(crate) fn format(command: FormatCommand) -> anyhow::Result<ExitStatus> {
41-
let mode = FormatMode::from_command(&command);
42-
43-
let mut resolver = PathResolver::new(Settings::default());
44-
45-
for DiscoveredSettings {
46-
directory,
47-
settings,
48-
} in discover_settings(&command.paths)?
49-
{
50-
resolver.add(&directory, settings);
14+
if let Some(status) = check_argument_consistency(&command) {
15+
return Ok(status);
5116
}
5217

53-
match mode {
54-
FormatMode::Write => {
55-
let errors = format_paths_write(&command.paths, &resolver);
56-
57-
for error in &errors {
58-
tracing::error!("{error}");
59-
}
60-
61-
if errors.is_empty() {
62-
Ok(ExitStatus::Success)
63-
} else {
64-
Ok(ExitStatus::Error)
65-
}
66-
}
67-
FormatMode::Check => {
68-
let (paths, errors) = format_paths_check(&command.paths, &resolver);
69-
70-
for error in &errors {
71-
tracing::error!("{error}");
72-
}
18+
let mode = FormatMode::from_command(&command);
7319

74-
inform_changed(&paths, &mut stderr().lock())?;
20+
match command.stdin_file_path {
21+
Some(path) => stdin::format(path, mode),
22+
None => paths::format(command.paths, mode),
23+
}
24+
}
7525

76-
if errors.is_empty() {
77-
if paths.is_empty() {
78-
Ok(ExitStatus::Success)
79-
} else {
80-
Ok(ExitStatus::Failure)
81-
}
82-
} else {
83-
Ok(ExitStatus::Error)
84-
}
85-
}
26+
fn check_argument_consistency(command: &FormatCommand) -> Option<ExitStatus> {
27+
if command.stdin_file_path.is_some() && !command.paths.is_empty() {
28+
tracing::error!(
29+
"Can't supply paths when reading from stdin: {paths}",
30+
paths = command
31+
.paths
32+
.iter()
33+
.map(|path| format!("'{path}'", path = path.display()))
34+
.collect::<Vec<String>>()
35+
.join(",")
36+
);
37+
return Some(ExitStatus::Error);
8638
}
39+
40+
None
8741
}
8842

8943
impl FormatMode {
@@ -95,134 +49,3 @@ impl FormatMode {
9549
}
9650
}
9751
}
98-
99-
fn inform_changed(paths: &[PathBuf], f: &mut impl Write) -> io::Result<()> {
100-
for path in paths.iter().sorted_unstable() {
101-
writeln!(
102-
f,
103-
"Would reformat: {path}",
104-
path = relativize_path(path).underline()
105-
)?;
106-
}
107-
Ok(())
108-
}
109-
110-
fn format_paths_write<P: AsRef<Path>>(
111-
paths: &[P],
112-
resolver: &PathResolver<Settings>,
113-
) -> Vec<FormatPathError> {
114-
let paths = discover_r_file_paths(paths, resolver, true);
115-
116-
paths
117-
.into_iter()
118-
.filter_map(|path| match path {
119-
Ok(path) => {
120-
let settings = resolver.resolve_or_fallback(&path);
121-
match format_path(&path, &settings.format) {
122-
Ok(file) => match write_path(&path, file) {
123-
Ok(()) => None,
124-
Err(err) => Some(FormatPathError::Write(path, err)),
125-
},
126-
Err(err) => Some(FormatPathError::FormatFile(path, err)),
127-
}
128-
}
129-
Err(err) => Some(err.into()),
130-
})
131-
.collect()
132-
}
133-
134-
fn format_paths_check<P: AsRef<Path>>(
135-
paths: &[P],
136-
resolver: &PathResolver<Settings>,
137-
) -> (Vec<PathBuf>, Vec<FormatPathError>) {
138-
let paths = discover_r_file_paths(paths, resolver, true);
139-
140-
paths
141-
.into_iter()
142-
.filter_map(|path| match path {
143-
Ok(path) => {
144-
let settings = resolver.resolve_or_fallback(&path);
145-
match format_path(&path, &settings.format) {
146-
Ok(file) => check_path(path, file).map(Ok),
147-
Err(err) => Some(Err(FormatPathError::FormatFile(path, err))),
148-
}
149-
}
150-
Err(err) => Some(Err(err.into())),
151-
})
152-
.partition_map(|result| match result {
153-
Ok(result) => Either::Left(result),
154-
Err(err) => Either::Right(err),
155-
})
156-
}
157-
158-
fn format_path<P: AsRef<Path>>(
159-
path: P,
160-
settings: &FormatSettings,
161-
) -> std::result::Result<FormattedFile, FormatFileError> {
162-
let path = path.as_ref();
163-
tracing::trace!("Formatting {path}", path = path.display());
164-
format_file(path, settings)
165-
}
166-
167-
/// Returns `Ok(())` if the format results were successfully written back, otherwise
168-
/// returns an error
169-
fn write_path<P: AsRef<Path>>(path: P, file: FormattedFile) -> io::Result<()> {
170-
match file {
171-
FormattedFile::Changed(file) => std::fs::write(path, file.new()),
172-
FormattedFile::Unchanged => Ok(()),
173-
}
174-
}
175-
176-
/// Returns `Some(path)` if a change occurred, otherwise returns `None`
177-
fn check_path(path: PathBuf, file: FormattedFile) -> Option<PathBuf> {
178-
match file {
179-
FormattedFile::Changed(_) => Some(path),
180-
FormattedFile::Unchanged => None,
181-
}
182-
}
183-
184-
impl Display for FormatPathError {
185-
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
186-
match self {
187-
Self::FormatFile(path, err) => match err {
188-
FormatFileError::Format(err) => write!(
189-
f,
190-
"Failed to format {path}: {err}",
191-
path = relativize_path(path).underline(),
192-
),
193-
FormatFileError::Read(err) => write!(
194-
f,
195-
"Failed to read {path}: {err}",
196-
path = relativize_path(path).underline(),
197-
),
198-
},
199-
Self::Ignore(err) => {
200-
if let ignore::Error::WithPath { path, .. } = err {
201-
write!(
202-
f,
203-
"Failed to format {path}: {err}",
204-
path = relativize_path(path).underline(),
205-
err = err
206-
.io_error()
207-
.map_or_else(|| err.to_string(), std::string::ToString::to_string)
208-
)
209-
} else {
210-
write!(
211-
f,
212-
"Encountered error: {err}",
213-
err = err
214-
.io_error()
215-
.map_or_else(|| err.to_string(), std::string::ToString::to_string)
216-
)
217-
}
218-
}
219-
Self::Write(path, err) => {
220-
write!(
221-
f,
222-
"Failed to write {path}: {err}",
223-
path = relativize_path(path).underline(),
224-
)
225-
}
226-
}
227-
}
228-
}

0 commit comments

Comments
 (0)