Skip to content

Commit 3bf997b

Browse files
committed
add emmylua_format
1 parent 58b3e83 commit 3bf997b

File tree

11 files changed

+376
-5
lines changed

11 files changed

+376
-5
lines changed

Cargo.lock

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,27 @@
11
[package]
22
name = "emmylua_code_style"
33
version = "0.1.0"
4-
edition = "2021"
4+
edition = "2024"
55

66
[dependencies]
77
serde.workspace = true
88
emmylua_parser.workspace = true
99
rowan.workspace = true
10+
serde_json.workspace = true
11+
serde_yml.workspace = true
12+
13+
[dependencies.clap]
14+
workspace = true
15+
optional = true
16+
17+
[dependencies.mimalloc]
18+
workspace = true
19+
optional = true
20+
21+
[[bin]]
22+
name = "emmylua_format"
23+
required-features = ["cli"]
24+
25+
[features]
26+
default = ["cli"]
27+
cli = ["dep:clap", "dep:mimalloc"]
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
use std::{
2+
fs,
3+
io::{self, Read, Write},
4+
path::PathBuf,
5+
process::exit,
6+
};
7+
8+
use clap::Parser;
9+
use emmylua_code_style::{LuaCodeStyle, cmd_args, reformat_lua_code};
10+
11+
#[global_allocator]
12+
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
13+
14+
fn read_stdin_to_string() -> io::Result<String> {
15+
let mut s = String::new();
16+
io::stdin().read_to_string(&mut s)?;
17+
Ok(s)
18+
}
19+
20+
fn format_content(content: &str, style: &LuaCodeStyle) -> String {
21+
reformat_lua_code(content, style)
22+
}
23+
24+
#[allow(unused)]
25+
fn process_file(
26+
path: &PathBuf,
27+
style: &LuaCodeStyle,
28+
write: bool,
29+
list_diff: bool,
30+
) -> io::Result<(bool, Option<String>)> {
31+
let original = fs::read_to_string(path)?;
32+
let formatted = format_content(&original, style);
33+
let changed = formatted != original;
34+
35+
if write && changed {
36+
fs::write(path, formatted)?;
37+
return Ok((true, None));
38+
}
39+
40+
if list_diff && changed {
41+
return Ok((true, Some(path.to_string_lossy().to_string())));
42+
}
43+
44+
Ok((changed, None))
45+
}
46+
47+
fn main() {
48+
let args = cmd_args::CliArgs::parse();
49+
50+
let mut exit_code = 0;
51+
52+
let style = match cmd_args::resolve_style(&args) {
53+
Ok(s) => s,
54+
Err(e) => {
55+
eprintln!("Error: {e}");
56+
exit(2);
57+
}
58+
};
59+
60+
let is_stdin = args.stdin || args.paths.is_empty();
61+
62+
if is_stdin {
63+
let content = match read_stdin_to_string() {
64+
Ok(s) => s,
65+
Err(e) => {
66+
eprintln!("Failed to read stdin: {e}");
67+
exit(2);
68+
}
69+
};
70+
71+
let formatted = format_content(&content, &style);
72+
let changed = formatted != content;
73+
74+
if args.check || args.list_different {
75+
if changed {
76+
exit_code = 1;
77+
}
78+
} else if let Some(out) = &args.output {
79+
if let Err(e) = fs::write(out, formatted) {
80+
eprintln!("Failed to write output to {out:?}: {e}");
81+
exit(2);
82+
}
83+
} else if args.write {
84+
eprintln!("--write with stdin requires --output <FILE>");
85+
exit(2);
86+
} else {
87+
let mut stdout = io::stdout();
88+
if let Err(e) = stdout.write_all(formatted.as_bytes()) {
89+
eprintln!("Failed to write to stdout: {e}");
90+
exit(2);
91+
}
92+
}
93+
94+
exit(exit_code);
95+
}
96+
97+
if args.paths.len() > 1 && args.output.is_some() {
98+
eprintln!("--output can only be used with a single input or stdin");
99+
exit(2);
100+
}
101+
102+
if args.paths.len() > 1 && !(args.write || args.check || args.list_different) {
103+
eprintln!("Multiple inputs require --write or --check");
104+
exit(2);
105+
}
106+
107+
let mut different_paths: Vec<String> = Vec::new();
108+
109+
for path in &args.paths {
110+
match fs::metadata(path) {
111+
Ok(meta) => {
112+
if !meta.is_file() {
113+
eprintln!("Skipping non-file path: {}", path.to_string_lossy());
114+
continue;
115+
}
116+
}
117+
Err(e) => {
118+
eprintln!("Cannot access {}: {e}", path.to_string_lossy());
119+
exit_code = 2;
120+
continue;
121+
}
122+
}
123+
124+
match fs::read_to_string(path) {
125+
Ok(original) => {
126+
let formatted = format_content(&original, &style);
127+
let changed = formatted != original;
128+
129+
if args.check || args.list_different {
130+
if changed {
131+
exit_code = 1;
132+
if args.list_different {
133+
different_paths.push(path.to_string_lossy().to_string());
134+
}
135+
}
136+
} else if args.write {
137+
if changed {
138+
if let Err(e) = fs::write(path, formatted) {
139+
eprintln!("Failed to write {}: {e}", path.to_string_lossy());
140+
exit_code = 2;
141+
}
142+
}
143+
} else if let Some(out) = &args.output {
144+
if let Err(e) = fs::write(out, formatted) {
145+
eprintln!("Failed to write output to {out:?}: {e}");
146+
exit(2);
147+
}
148+
} else {
149+
// Single file without write/check: print to stdout
150+
let mut stdout = io::stdout();
151+
if let Err(e) = stdout.write_all(formatted.as_bytes()) {
152+
eprintln!("Failed to write to stdout: {e}");
153+
exit(2);
154+
}
155+
}
156+
}
157+
Err(e) => {
158+
eprintln!("Failed to read {}: {e}", path.to_string_lossy());
159+
exit_code = 2;
160+
}
161+
}
162+
}
163+
164+
if args.list_different && !different_paths.is_empty() {
165+
for p in different_paths {
166+
println!("{p}");
167+
}
168+
}
169+
170+
exit(exit_code);
171+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
use std::{fs, path::PathBuf};
2+
3+
use clap::{ArgGroup, Parser};
4+
5+
use crate::styles::{LuaCodeStyle, LuaIndent};
6+
7+
#[derive(Debug, Clone, Parser)]
8+
#[command(
9+
name = "emmylua_format",
10+
version,
11+
about = "Format Lua source code using EmmyLua code style rules",
12+
disable_help_subcommand = true
13+
)]
14+
#[command(group(
15+
ArgGroup::new("indent_choice")
16+
.args(["tab", "spaces"])
17+
.multiple(false)
18+
))]
19+
pub struct CliArgs {
20+
/// Input paths to format (files only). If omitted, reads from stdin.
21+
#[arg(value_name = "PATH", value_hint = clap::ValueHint::FilePath)]
22+
pub paths: Vec<PathBuf>,
23+
24+
/// Read source from stdin instead of files
25+
#[arg(long)]
26+
pub stdin: bool,
27+
28+
/// Write formatted result back to the file(s)
29+
#[arg(long)]
30+
pub write: bool,
31+
32+
/// Check if files would be reformatted. Exit with code 1 if any would change.
33+
#[arg(long)]
34+
pub check: bool,
35+
36+
/// Print paths of files that would be reformatted
37+
#[arg(long, alias = "list-different")]
38+
pub list_different: bool,
39+
40+
/// Write output to a specific file (only with a single input or stdin)
41+
#[arg(short, long, value_name = "FILE", value_hint = clap::ValueHint::FilePath)]
42+
pub output: Option<PathBuf>,
43+
44+
/// Load style config from a file (json/yml/yaml)
45+
#[arg(long, value_name = "FILE", value_hint = clap::ValueHint::FilePath)]
46+
pub config: Option<PathBuf>,
47+
48+
/// Use tabs for indentation
49+
#[arg(long)]
50+
pub tab: bool,
51+
52+
/// Use N spaces for indentation (mutually exclusive with --tab)
53+
#[arg(long, value_name = "N")]
54+
pub spaces: Option<usize>,
55+
56+
/// Set maximum line width
57+
#[arg(long, value_name = "N")]
58+
pub max_line_width: Option<usize>,
59+
}
60+
61+
pub fn resolve_style(args: &CliArgs) -> Result<LuaCodeStyle, String> {
62+
let mut style = if let Some(cfg) = &args.config {
63+
let content = fs::read_to_string(cfg)
64+
.map_err(|e| format!("读取配置失败: {}: {e}", cfg.to_string_lossy()))?;
65+
let ext = cfg
66+
.extension()
67+
.and_then(|s| s.to_str())
68+
.map(|s| s.to_ascii_lowercase())
69+
.unwrap_or_default();
70+
match ext.as_str() {
71+
"json" => serde_json::from_str::<LuaCodeStyle>(&content)
72+
.map_err(|e| format!("解析 JSON 配置失败: {e}"))?,
73+
"yml" | "yaml" => serde_yml::from_str::<LuaCodeStyle>(&content)
74+
.map_err(|e| format!("解析 YAML 配置失败: {e}"))?,
75+
_ => {
76+
// Unknown extension, try JSON first then YAML
77+
match serde_json::from_str::<LuaCodeStyle>(&content) {
78+
Ok(v) => v,
79+
Err(_) => serde_yml::from_str::<LuaCodeStyle>(&content)
80+
.map_err(|e| format!("未知扩展名,按 JSON/YAML 解析均失败: {e}"))?,
81+
}
82+
}
83+
}
84+
} else {
85+
LuaCodeStyle::default()
86+
};
87+
88+
// Indent overrides
89+
match (args.tab, args.spaces) {
90+
(true, Some(_)) => return Err("--tab 与 --spaces 不能同时使用".into()),
91+
(true, None) => style.indent = LuaIndent::Tab,
92+
(false, Some(n)) => style.indent = LuaIndent::Space(n),
93+
_ => {}
94+
}
95+
96+
if let Some(w) = args.max_line_width {
97+
style.max_line_width = w;
98+
}
99+
100+
Ok(style)
101+
}

crates/emmylua_code_style/src/format/formatter_context.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,23 @@ impl FormatterContext {
2121
self.text.pop();
2222
}
2323
}
24+
25+
pub fn get_last_whitespace_count(&self) -> usize {
26+
let mut count = 0;
27+
for ch in self.text.chars().rev() {
28+
if ch == ' ' {
29+
count += 1;
30+
} else {
31+
break;
32+
}
33+
}
34+
count
35+
}
36+
37+
pub fn reset_whitespace_to(&mut self, n: usize) {
38+
self.reset_whitespace();
39+
if n > 0 {
40+
self.text.push_str(&" ".repeat(n));
41+
}
42+
}
2443
}

crates/emmylua_code_style/src/format/mod.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,15 @@ impl LuaFormatter {
6161
continue;
6262
}
6363
}
64+
(Some(TokenExpected::MaxSpace(n)), LuaTokenKind::TkWhitespace) => {
65+
if !context.is_line_first_token {
66+
let white_space_len = token.text().chars().count();
67+
if white_space_len > n {
68+
context.reset_whitespace_to(n);
69+
continue;
70+
}
71+
}
72+
}
6473
(_, LuaTokenKind::TkEndOfLine) => {
6574
// No space expected
6675
context.reset_whitespace();
@@ -85,6 +94,14 @@ impl LuaFormatter {
8594
context.text.push_str(&" ".repeat(*n));
8695
}
8796
}
97+
TokenExpected::MaxSpace(n) => {
98+
if !context.is_line_first_token {
99+
let current_spaces = context.get_last_whitespace_count();
100+
if current_spaces > *n {
101+
context.reset_whitespace_to(*n);
102+
}
103+
}
104+
}
88105
}
89106
}
90107

crates/emmylua_code_style/src/format/syntax_node_change.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ pub enum TokenNodeChange {
1111
#[derive(Debug, Clone, Copy)]
1212
pub enum TokenExpected {
1313
Space(usize),
14+
MaxSpace(usize),
1415
}

crates/emmylua_code_style/src/lib.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1+
pub mod cmd_args;
12
mod format;
23
mod style_ruler;
34
mod styles;
45
mod test;
56

67
use emmylua_parser::{LuaAst, LuaParser, ParserConfig};
7-
use styles::LuaCodeStyle;
88

99
pub fn reformat_lua_code(code: &str, styles: &LuaCodeStyle) -> String {
1010
let tree = LuaParser::parse(code, ParserConfig::default());
@@ -21,3 +21,6 @@ pub fn reformat_node(node: &LuaAst, styles: &LuaCodeStyle) -> String {
2121
let formatted_text = formatter.get_formatted_text();
2222
formatted_text
2323
}
24+
25+
// Re-export commonly used types for consumers/binaries
26+
pub use styles::LuaCodeStyle;

0 commit comments

Comments
 (0)