Skip to content

Commit f6b95e8

Browse files
committed
implement formatter for xmake
1 parent 8d26316 commit f6b95e8

File tree

13 files changed

+359
-2
lines changed

13 files changed

+359
-2
lines changed

Cargo.lock

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ members = [
88
# local
99
xmake_code_analysis = { path = "crates/xmake_code_analysis", version = "0.2.0" }
1010
xmake_ls = { path = "crates/xmake_ls", version = "0.2.0" }
11+
xmake_formatter = { path = "crates/xmake_formatter", version = "0.2.0" }
1112
xmake_wrapper = { path = "crates/xmake_wrapper", version = "0.1.0" }
1213

1314
# external

crates/xmake_formatter/Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
[package]
2+
name = "xmake_formatter"
3+
version = "0.2.0"
4+
edition = "2024"
5+
6+
[dependencies]
7+
serde.workspace = true
8+
emmylua_parser.workspace = true
9+
rowan.workspace = true
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
use crate::format::TokenExpected;
2+
3+
#[derive(Debug)]
4+
pub struct FormatterContext {
5+
pub current_expected: Option<TokenExpected>,
6+
pub is_line_first_token: bool,
7+
pub text: String,
8+
}
9+
10+
impl FormatterContext {
11+
pub fn new() -> Self {
12+
Self {
13+
current_expected: None,
14+
is_line_first_token: true,
15+
text: String::new(),
16+
}
17+
}
18+
19+
// 从text的末尾移除所有空白字符, 仅包含空格, 但是不能新建text
20+
pub fn reset_whitespace(&mut self) {
21+
while self.text.ends_with(' ') {
22+
self.text.pop();
23+
}
24+
}
25+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
mod formatter_context;
2+
mod syntax_node_change;
3+
4+
use std::collections::HashMap;
5+
6+
use emmylua_parser::{LuaAst, LuaAstNode, LuaSyntaxId, LuaTokenKind};
7+
use rowan::NodeOrToken;
8+
9+
use crate::format::formatter_context::FormatterContext;
10+
pub use crate::format::syntax_node_change::{TokenExpected, TokenNodeChange};
11+
12+
#[allow(unused)]
13+
#[derive(Debug)]
14+
pub struct XmakeFormatter {
15+
root: LuaAst,
16+
token_changes: HashMap<LuaSyntaxId, TokenNodeChange>,
17+
token_left_expected: HashMap<LuaSyntaxId, TokenExpected>,
18+
token_right_expected: HashMap<LuaSyntaxId, TokenExpected>,
19+
}
20+
21+
#[allow(unused)]
22+
impl XmakeFormatter {
23+
pub fn new(root: LuaAst) -> Self {
24+
Self {
25+
root,
26+
token_changes: HashMap::new(),
27+
token_left_expected: HashMap::new(),
28+
token_right_expected: HashMap::new(),
29+
}
30+
}
31+
32+
pub fn add_token_change(&mut self, syntax_id: LuaSyntaxId, change: TokenNodeChange) {
33+
self.token_changes.insert(syntax_id, change);
34+
}
35+
36+
pub fn add_token_left_expected(&mut self, syntax_id: LuaSyntaxId, expected: TokenExpected) {
37+
self.token_left_expected.insert(syntax_id, expected);
38+
}
39+
40+
pub fn add_token_right_expected(&mut self, syntax_id: LuaSyntaxId, expected: TokenExpected) {
41+
self.token_right_expected.insert(syntax_id, expected);
42+
}
43+
44+
pub fn get_token_change(&self, syntax_id: &LuaSyntaxId) -> Option<&TokenNodeChange> {
45+
self.token_changes.get(syntax_id)
46+
}
47+
48+
pub fn get_root(&self) -> LuaAst {
49+
self.root.clone()
50+
}
51+
52+
pub fn get_formatted_text(&self) -> String {
53+
let mut context = FormatterContext::new();
54+
for node_or_token in self.root.syntax().descendants_with_tokens() {
55+
if let NodeOrToken::Token(token) = node_or_token {
56+
let token_kind = token.kind().to_token();
57+
match (context.current_expected.take(), token_kind) {
58+
(Some(TokenExpected::Space(n)), LuaTokenKind::TkWhitespace) => {
59+
if !context.is_line_first_token {
60+
context.text.push_str(&" ".repeat(n));
61+
continue;
62+
}
63+
}
64+
(_, LuaTokenKind::TkEndOfLine) => {
65+
// No space expected
66+
context.reset_whitespace();
67+
context.text.push_str("\n");
68+
context.is_line_first_token = true;
69+
continue;
70+
}
71+
(Some(TokenExpected::Space(n)), _) => {
72+
if !context.is_line_first_token {
73+
context.text.push_str(&" ".repeat(n));
74+
}
75+
}
76+
_ => {}
77+
}
78+
79+
let syntax_id = LuaSyntaxId::from_token(&token);
80+
if let Some(expected) = self.token_left_expected.get(&syntax_id) {
81+
match expected {
82+
TokenExpected::Space(n) => {
83+
if !context.is_line_first_token {
84+
context.reset_whitespace();
85+
context.text.push_str(&" ".repeat(*n));
86+
}
87+
}
88+
}
89+
}
90+
91+
if token_kind != LuaTokenKind::TkWhitespace {
92+
context.is_line_first_token = false;
93+
}
94+
95+
if let Some(change) = self.token_changes.get(&syntax_id) {
96+
match change {
97+
TokenNodeChange::Remove => continue,
98+
TokenNodeChange::AddLeft(s) => {
99+
context.text.push_str(s);
100+
context.text.push_str(&token.text());
101+
}
102+
TokenNodeChange::AddRight(s) => {
103+
context.text.push_str(&token.text());
104+
context.text.push_str(s);
105+
}
106+
TokenNodeChange::ReplaceWith(s) => {
107+
context.text.push_str(s);
108+
}
109+
}
110+
} else {
111+
context.text.push_str(&token.text());
112+
}
113+
114+
context.current_expected = self.token_right_expected.get(&syntax_id).cloned();
115+
}
116+
}
117+
118+
context.text
119+
}
120+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#[allow(unused)]
2+
#[derive(Debug)]
3+
pub enum TokenNodeChange {
4+
Remove,
5+
AddLeft(String),
6+
AddRight(String),
7+
ReplaceWith(String),
8+
}
9+
10+
#[allow(unused)]
11+
#[derive(Debug, Clone, Copy)]
12+
pub enum TokenExpected {
13+
Space(usize),
14+
}

crates/xmake_formatter/src/lib.rs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
mod format;
2+
mod style_ruler;
3+
mod test;
4+
5+
use emmylua_parser::{LuaAst, LuaParser, ParserConfig};
6+
7+
pub fn reformat_xmake_code(code: &str) -> String {
8+
let tree = LuaParser::parse(code, ParserConfig::default());
9+
10+
let mut formatter = format::XmakeFormatter::new(LuaAst::LuaChunk(tree.get_chunk_node()));
11+
style_ruler::apply_styles(&mut formatter);
12+
let formatted_text = formatter.get_formatted_text();
13+
formatted_text
14+
}
15+
16+
pub fn reformat_node(node: &LuaAst) -> String {
17+
let mut formatter = format::XmakeFormatter::new(node.clone());
18+
style_ruler::apply_styles(&mut formatter);
19+
let formatted_text = formatter.get_formatted_text();
20+
formatted_text
21+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
use emmylua_parser::{LuaAstNode, LuaSyntaxId, LuaSyntaxKind, LuaSyntaxToken, LuaTokenKind};
2+
use rowan::NodeOrToken;
3+
4+
use crate::format::{TokenExpected, XmakeFormatter};
5+
6+
use super::StyleRuler;
7+
8+
pub struct BasicSpaceRuler;
9+
10+
impl StyleRuler for BasicSpaceRuler {
11+
fn apply_style(f: &mut XmakeFormatter) {
12+
let root = f.get_root();
13+
for node_or_token in root.syntax().descendants_with_tokens() {
14+
if let NodeOrToken::Token(token) = node_or_token {
15+
let syntax_id = LuaSyntaxId::from_token(&token);
16+
match token.kind().to_token() {
17+
LuaTokenKind::TkLeftParen | LuaTokenKind::TkLeftBracket => {
18+
f.add_token_right_expected(syntax_id, TokenExpected::Space(0));
19+
}
20+
LuaTokenKind::TkRightBracket | LuaTokenKind::TkRightParen => {
21+
f.add_token_left_expected(syntax_id, TokenExpected::Space(0));
22+
}
23+
LuaTokenKind::TkLeftBrace => {
24+
f.add_token_right_expected(syntax_id, TokenExpected::Space(1));
25+
}
26+
LuaTokenKind::TkRightBrace => {
27+
f.add_token_left_expected(syntax_id, TokenExpected::Space(1));
28+
}
29+
LuaTokenKind::TkComma => {
30+
f.add_token_left_expected(syntax_id, TokenExpected::Space(0));
31+
f.add_token_right_expected(syntax_id, TokenExpected::Space(1));
32+
}
33+
LuaTokenKind::TkPlus | LuaTokenKind::TkMinus => {
34+
if is_parent_syntax(&token, LuaSyntaxKind::UnaryExpr) {
35+
f.add_token_right_expected(syntax_id, TokenExpected::Space(0));
36+
continue;
37+
}
38+
39+
f.add_token_left_expected(syntax_id, TokenExpected::Space(1));
40+
f.add_token_right_expected(syntax_id, TokenExpected::Space(1));
41+
}
42+
LuaTokenKind::TkLt => {
43+
if is_parent_syntax(&token, LuaSyntaxKind::Attribute) {
44+
f.add_token_left_expected(syntax_id, TokenExpected::Space(1));
45+
f.add_token_right_expected(syntax_id, TokenExpected::Space(0));
46+
continue;
47+
}
48+
49+
f.add_token_left_expected(syntax_id, TokenExpected::Space(1));
50+
f.add_token_right_expected(syntax_id, TokenExpected::Space(1));
51+
}
52+
LuaTokenKind::TkGt => {
53+
if is_parent_syntax(&token, LuaSyntaxKind::Attribute) {
54+
f.add_token_left_expected(syntax_id, TokenExpected::Space(0));
55+
f.add_token_right_expected(syntax_id, TokenExpected::Space(1));
56+
continue;
57+
}
58+
59+
f.add_token_left_expected(syntax_id, TokenExpected::Space(1));
60+
f.add_token_right_expected(syntax_id, TokenExpected::Space(1));
61+
}
62+
LuaTokenKind::TkMul
63+
| LuaTokenKind::TkDiv
64+
| LuaTokenKind::TkIDiv
65+
| LuaTokenKind::TkMod
66+
| LuaTokenKind::TkPow
67+
| LuaTokenKind::TkConcat
68+
| LuaTokenKind::TkAssign
69+
| LuaTokenKind::TkBitAnd
70+
| LuaTokenKind::TkBitOr
71+
| LuaTokenKind::TkBitXor
72+
| LuaTokenKind::TkEq
73+
| LuaTokenKind::TkGe
74+
| LuaTokenKind::TkLe
75+
| LuaTokenKind::TkNe
76+
| LuaTokenKind::TkAnd
77+
| LuaTokenKind::TkOr
78+
| LuaTokenKind::TkShl
79+
| LuaTokenKind::TkShr => {
80+
f.add_token_left_expected(syntax_id, TokenExpected::Space(1));
81+
f.add_token_right_expected(syntax_id, TokenExpected::Space(1));
82+
}
83+
LuaTokenKind::TkColon | LuaTokenKind::TkDot => {
84+
f.add_token_left_expected(syntax_id, TokenExpected::Space(0));
85+
f.add_token_right_expected(syntax_id, TokenExpected::Space(0));
86+
}
87+
LuaTokenKind::TkLocal
88+
| LuaTokenKind::TkFunction
89+
| LuaTokenKind::TkIf
90+
| LuaTokenKind::TkWhile
91+
| LuaTokenKind::TkFor
92+
| LuaTokenKind::TkRepeat
93+
| LuaTokenKind::TkReturn
94+
| LuaTokenKind::TkDo
95+
| LuaTokenKind::TkElseIf
96+
| LuaTokenKind::TkElse
97+
| LuaTokenKind::TkThen
98+
| LuaTokenKind::TkUntil
99+
| LuaTokenKind::TkIn
100+
| LuaTokenKind::TkNot => {
101+
f.add_token_left_expected(syntax_id, TokenExpected::Space(1));
102+
f.add_token_right_expected(syntax_id, TokenExpected::Space(1));
103+
}
104+
_ => {}
105+
}
106+
}
107+
}
108+
}
109+
}
110+
111+
fn is_parent_syntax(token: &LuaSyntaxToken, kind: LuaSyntaxKind) -> bool {
112+
if let Some(parent) = token.parent() {
113+
return parent.kind().to_syntax() == kind;
114+
}
115+
false
116+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
mod basic_space;
2+
3+
use crate::format::XmakeFormatter;
4+
5+
#[allow(unused)]
6+
pub fn apply_styles(formatter: &mut XmakeFormatter) {
7+
apply_style::<basic_space::BasicSpaceRuler>(formatter);
8+
}
9+
10+
pub trait StyleRuler {
11+
/// Apply the style rules to the formatter
12+
fn apply_style(formatter: &mut XmakeFormatter);
13+
}
14+
15+
pub fn apply_style<T: StyleRuler>(formatter: &mut XmakeFormatter) {
16+
T::apply_style(formatter)
17+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#[cfg(test)]
2+
mod test {
3+
use crate::reformat_xmake_code;
4+
5+
#[test]
6+
fn test_reformat_lua_code() {
7+
let code = r#"
8+
local a = 1
9+
local b = 2
10+
local c = a+b
11+
local e =- b
12+
local ccc = a > 2 and b< 3
13+
print(c)
14+
"#;
15+
16+
let formatted_code = reformat_xmake_code(code);
17+
println!("Formatted code:\n{}", formatted_code);
18+
}
19+
}

0 commit comments

Comments
 (0)