diff --git a/Cargo.lock b/Cargo.lock index 8fec50b..6c77331 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,15 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "amber-fmt" +version = "0.1.0" +dependencies = [ + "amber_grammar", + "amber_types", + "thiserror", +] + [[package]] name = "amber-lsp" version = "0.1.14" @@ -38,7 +47,7 @@ dependencies = [ "ropey", "rustc-hash", "serde_json", - "thiserror 2.0.17", + "thiserror", "tokio", "tower-lsp-server", "tracing", @@ -59,7 +68,7 @@ dependencies = [ "rangemap", "ropey", "rustc-hash", - "thiserror 2.0.17", + "thiserror", "tokio", "tower-lsp-server", "tracing", @@ -121,22 +130,22 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -168,9 +177,9 @@ checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "bytes" -version = "1.10.1" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "capitalize" @@ -180,9 +189,9 @@ checksum = "6b5271031022835ee8c7582fe67403bd6cb3d962095787af7921027234bab5bf" [[package]] name = "cc" -version = "1.2.48" +version = "1.2.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" +checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215" dependencies = [ "find-msvc-tools", "shlex", @@ -210,9 +219,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.51" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" dependencies = [ "clap_builder", "clap_derive", @@ -220,9 +229,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.51" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" dependencies = [ "anstream", "anstyle", @@ -455,9 +464,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heck" @@ -467,9 +476,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "heraclitus-compiler" -version = "1.8.3" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "817342b2c5aae155ce8b61fec450d7e4c6f11dbd0e27126a104c245066d9c0a0" +checksum = "e72670042e09178f5cf816dd5d647e85c503882354de9c76eee396256355633e" dependencies = [ "capitalize", "colored", @@ -503,19 +512,19 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.16.1", ] [[package]] name = "insta" -version = "1.43.2" +version = "1.44.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46fdb647ebde000f43b5b53f773c30cf9b0cb4300453208713fa38b2c70935a0" +checksum = "b5c943d4415edd8153251b6f197de5eb1640e56d84e8d9159bea190421c73698" dependencies = [ "console", "once_cell", @@ -543,9 +552,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.177" +version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "lock_api" @@ -558,9 +567,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "logos" @@ -616,9 +625,9 @@ checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "mio" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "wasi", @@ -780,9 +789,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.41" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] @@ -804,9 +813,9 @@ checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" [[package]] name = "rangemap" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93e7e49bb0bf967717f7bd674458b3d6b0c5f48ec7e3038166026a69fc22223" +checksum = "acbbbbea733ec66275512d0b9694f34102e7d5406fdbe2ad8d21b28dce92887c" [[package]] name = "redox_syscall" @@ -939,9 +948,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" dependencies = [ "libc", ] @@ -1007,9 +1016,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.108" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -1022,33 +1031,13 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - [[package]] name = "thiserror" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.17", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", + "thiserror-impl", ] [[package]] @@ -1192,9 +1181,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -1203,21 +1192,21 @@ dependencies = [ [[package]] name = "tracing-appender" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf" dependencies = [ "crossbeam-channel", - "thiserror 1.0.69", + "thiserror", "time", "tracing-subscriber", ] [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", @@ -1226,9 +1215,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" dependencies = [ "once_cell", "valuable", @@ -1247,9 +1236,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.20" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ "nu-ansi-term", "sharded-slab", diff --git a/Cargo.toml b/Cargo.toml index fc51249..e0b54dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,10 @@ [workspace] members = [ - "crates/types", - "crates/grammar", - "crates/analysis", - "crates/server", + "crates/types", + "crates/grammar", + "crates/analysis", + "crates/server", + "crates/formatter", ] resolver = "2" diff --git a/crates/formatter/Cargo.toml b/crates/formatter/Cargo.toml new file mode 100644 index 0000000..270fe4e --- /dev/null +++ b/crates/formatter/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "amber-fmt" +version = "0.1.0" +edition = "2024" + +[dependencies] +amber_types = { path = "../types" } +amber_grammar = { path = "../grammar" } +thiserror = "2.0.12" diff --git a/crates/formatter/src/alpha040/expression.rs b/crates/formatter/src/alpha040/expression.rs new file mode 100644 index 0000000..519e070 --- /dev/null +++ b/crates/formatter/src/alpha040/expression.rs @@ -0,0 +1,191 @@ +use crate::{ + SpanTextOutput, TextOutput, + alpha040::Gen, + format::{FmtContext, Output}, +}; +use amber_grammar::{Span, alpha040::Expression}; + +impl TextOutput for Expression { + fn output(&self, span: &Span, output: &mut Output, ctx: &mut FmtContext) { + fn char_separated( + output: &mut Output, + ctx: &mut FmtContext, + rhs: &impl SpanTextOutput, + middle: char, + lhs: &impl SpanTextOutput, + ) { + output.output(ctx, rhs); + output.space(); + output.char(middle); + output.space(); + output.output(ctx, lhs); + } + + fn string_separated( + output: &mut Output, + ctx: &mut FmtContext, + rhs: &impl SpanTextOutput, + middle: &str, + lhs: &impl SpanTextOutput, + ) { + output.output(ctx, rhs); + output.space(); + output.text(middle); + output.space(); + output.output(ctx, lhs); + } + + fn output_separated( + output: &mut Output, + ctx: &mut FmtContext, + rhs: &impl SpanTextOutput, + middle: &impl SpanTextOutput, + lhs: &impl SpanTextOutput, + ) { + output.output(ctx, rhs); + output.space(); + output.output(ctx, middle); + output.space(); + output.output(ctx, lhs); + } + + match self { + Expression::Number(num) => { + output.output(ctx, num); + } + Expression::Boolean(boolean) => { + output.output(ctx, boolean); + } + Expression::Text(_) => { + // Take raw text from file, as string content should not be modified + output.end_span(span); + } + Expression::Parentheses(parentheses) => { + output.output(ctx, parentheses); + } + Expression::Var(var) => output.end_output(ctx, var), + Expression::Add(rhs, lhs) => char_separated(output, ctx, rhs, '+', lhs), + Expression::Subtract(rhs, lhs) => char_separated(output, ctx, rhs, '-', lhs), + Expression::Multiply(rhs, lhs) => char_separated(output, ctx, rhs, '*', lhs), + Expression::Divide(rhs, lhs) => char_separated(output, ctx, rhs, '/', lhs), + Expression::Modulo(rhs, lhs) => char_separated(output, ctx, rhs, '%', lhs), + Expression::Neg(neg, lhs) => { + output.output(ctx, neg); + output.output(ctx, lhs); + } + Expression::And(rhs, and, lhs) => output_separated(output, ctx, rhs, and, lhs), + Expression::Or(rhs, or, lhs) => output_separated(output, ctx, rhs, or, lhs), + Expression::Gt(rhs, lhs) => char_separated(output, ctx, rhs, '>', lhs), + Expression::Ge(rhs, lhs) => string_separated(output, ctx, rhs, ">=", lhs), + Expression::Lt(rhs, lhs) => char_separated(output, ctx, rhs, '<', lhs), + Expression::Le(rhs, lhs) => string_separated(output, ctx, rhs, ">=", lhs), + Expression::Eq(rhs, lhs) => string_separated(output, ctx, rhs, "==", lhs), + Expression::Neq(rhs, lhs) => string_separated(output, ctx, rhs, "!=", lhs), + Expression::Not(not, lhs) => { + output.output(ctx, not); + output.space(); + output.output(ctx, lhs); + } + Expression::Ternary(condition, then, if_then, r#else, if_else) => { + // TODO(tye-exe): Allow single line ternary if short enough. Use given span to measure length? + output.increase_indentation(); + output.output(ctx, condition); + output.newline(); + output.output(ctx, then); + output.space(); + output.output(ctx, if_then); + output.newline(); + output.output(ctx, r#else); + output.space(); + output.output(ctx, if_else); + output.decrease_indentation(); + } + Expression::FunctionInvocation(modifiers, function_name, args, failure_handler) => { + for modifier in modifiers { + output.output(ctx, modifier); + output.space(); + } + + output.output(ctx, function_name).char('('); + + for arg in args.iter().take(args.len().saturating_sub(1)) { + output.output(ctx, arg).char(',').space(); + } + if let Some(arg) = args.last() { + output.output(ctx, arg); + } + + output.char(')'); + + if let Some(failure_handler) = failure_handler { + output.output(ctx, failure_handler); + } + } + Expression::Command(modifiers, commands, failure_handler) => { + for modifier in modifiers { + output.output(ctx, modifier); + output.space(); + } + + // Do not format bash commands + for command in commands { + output.end_span(&command.1); + } + + if let Some(failure_handler) = failure_handler { + output.space().output(ctx, failure_handler); + } + } + Expression::Array(array) => { + output.char('['); + for expression in array { + output.output(ctx, expression); + output.char(','); + output.space(); + } + output.remove_space(); + output.char(']'); + } + Expression::Range(lhs, rhs) => { + output.output(ctx, lhs); + output.text(".."); + output.output(ctx, rhs); + } + Expression::Null => { + output.text("Null"); + } + Expression::Cast(lhs, r#as, rhs) => string_separated(output, ctx, rhs, &r#as.0, lhs), + Expression::Status => { + output.text("status"); + } + Expression::Nameof(name_of, variable) => { + output.output(ctx, name_of); + output.space(); + output.output(ctx, variable); + } + Expression::Is(lhs, is, rhs) => { + output + .output(ctx, lhs) + .space() + .output(ctx, is) + .space() + .output(ctx, rhs); + } + Expression::ArrayIndex(array, index) => output + .output(ctx, array) + .char('[') + .output(ctx, index) + .end_char(']'), + Expression::Exit(exit, value) => { + output.output(ctx, exit); + + if let Some(value) = value { + output.space().output(ctx, value); + } + } + Expression::Error => { + output.error(span); + } + } + } +} diff --git a/crates/formatter/src/alpha040/global_statement.rs b/crates/formatter/src/alpha040/global_statement.rs new file mode 100644 index 0000000..fa01ca7 --- /dev/null +++ b/crates/formatter/src/alpha040/global_statement.rs @@ -0,0 +1,166 @@ +use crate::{ + TextOutput, + alpha040::Gen, + format::{FmtContext, Output}, +}; +use amber_grammar::{ + Span, + alpha040::{GlobalStatement, Statement}, +}; + +impl TextOutput for GlobalStatement { + fn output(&self, span: &Span, output: &mut Output, ctx: &mut FmtContext) { + match self { + GlobalStatement::Import(public, import, content, from, path) => { + if public.0 { + output.text("pub "); + } + + output + .output(ctx, import) + .space() + .output(ctx, content) + .space() + .output(ctx, from) + .space() + .char('"') + .output(ctx, path) + .char('"') + .newline(); + + if ctx + .next_global() + .is_some_and(|next| !matches!(next.0, GlobalStatement::Import(..))) + { + output.newline(); + } + + if let Some(next_global) = ctx.next_global() + && matches!(next_global.0, GlobalStatement::Import(..)) + { + ctx.allow_newline(output, span.end..=next_global.1.start); + } + } + GlobalStatement::FunctionDefinition( + compiler_flags, + public, + function_keyword, + name, + args, + return_type, + contents, + ) => { + if ctx.previous_global().is_some() { + output.newline(); + } + + for flag in compiler_flags { + output.output(ctx, flag); + output.newline(); + } + + if public.0 { + output.text("pub "); + } + + output.output(ctx, function_keyword); + output.space(); + output.output(ctx, name); + + output.char('('); + // Handle adding variables with proper spacing + { + for arg in args.iter().take(args.len().saturating_sub(1)) { + output.output(ctx, arg); + output.char(','); + output.space(); + } + + if let Some(arg) = args.last() { + output.output(ctx, arg); + } + } + output.char(')'); + + if let Some(returns) = return_type { + output.char(':').space().output(ctx, returns); + } + + output.space().char('{').increase_indentation().newline(); + + let mut last_span_end = None; + for content in contents { + if let Some(last_span_end) = last_span_end { + if !matches!(content.0, Statement::Comment(..)) + || ctx.source_has_newline(last_span_end..=content.1.start) + { + output.end_newline() + } + + ctx.allow_newline(output, last_span_end..=content.1.start); + } + + output.end_output(ctx, content); + last_span_end = Some(content.1.end); + } + + output + .remove_trailing_whitespace() + .decrease_indentation() + .newline() + .char('}'); + + if ctx.next_global().is_some() { + output.newline().newline(); + } + } + GlobalStatement::Main(main, args, statements) => { + if ctx.previous_global().is_some() { + output.newline(); + } + + output.output(ctx, main); + + if let Some(args) = args { + output.char('(').output(ctx, args).char(')'); + } + output.space().char('{').increase_indentation().newline(); + + let mut last_span_end = None; + for statement in statements { + if let Some(last_span_end) = last_span_end { + if !matches!(statement.0, Statement::Comment(..)) + || ctx.source_has_newline(last_span_end..=statement.1.start) + { + output.end_newline() + } + + ctx.allow_newline(output, last_span_end..=statement.1.start); + } + + output.output(ctx, statement); + last_span_end = Some(statement.1.end); + } + + output + .remove_trailing_whitespace() + .decrease_indentation() + .newline() + .char('}'); + + if ctx.next_global().is_some() { + output.newline().newline(); + } + } + GlobalStatement::Statement(statement) => { + output.output(ctx, statement).newline(); + + if let Some(next_global) = ctx.next_global() + && matches!(next_global.0, GlobalStatement::Statement(..)) + { + ctx.allow_newline(output, span.end..=next_global.1.start); + } + } + } + } +} diff --git a/crates/formatter/src/alpha040/mod.rs b/crates/formatter/src/alpha040/mod.rs new file mode 100644 index 0000000..3b6b95b --- /dev/null +++ b/crates/formatter/src/alpha040/mod.rs @@ -0,0 +1,235 @@ +use crate::{FmtContext, Output, SpanTextOutput, TextOutput, fragments::CommentVariant}; +use amber_grammar::{ + CommandModifier, CompilerFlag, + alpha040::{ + Block, Comment, ElseCondition, FailureHandler, FunctionArgument, GlobalStatement, + IfChainContent, IfCondition, ImportContent, InterpolatedCommand, IterLoopVars, + VariableInitType, + }, +}; +use amber_types::{DataType, token::Span}; + +mod expression; +mod global_statement; +mod statement; + +type Gen = (GlobalStatement, Span); + +impl TextOutput for ImportContent { + fn output(&self, _span: &Span, output: &mut Output, ctx: &mut FmtContext) { + match self { + ImportContent::ImportAll => { + output.char('*'); + } + ImportContent::ImportSpecific(items) => { + output.text("{ "); + + for identifier in items.iter().take(items.len().saturating_sub(1)) { + output.output(ctx, identifier).char(',').space(); + } + if let Some(item) = items.last() { + output.output(ctx, item).end_space(); + } + + output.char('}'); + } + } + } +} + +impl TextOutput for FunctionArgument { + fn output(&self, span: &Span, output: &mut Output, ctx: &mut FmtContext) { + match self { + FunctionArgument::Generic(is_ref, text) => { + if is_ref.0 { + output.text("ref"); + output.space(); + } + output.output(ctx, text); + } + FunctionArgument::Optional(is_ref, text, data_type, expression) => { + let is_ref = is_ref.0; + if is_ref { + output.text("ref"); + output.space(); + } + + output.output(ctx, text); + + if let Some(data_type) = data_type { + output.char(':').space().output(ctx, data_type); + } + + output.space().char('=').space().output(ctx, expression); + } + FunctionArgument::Typed(is_ref, text, data_type) => { + let is_ref = is_ref.0; + if is_ref { + output.text("ref"); + output.space(); + } + + output + .output(ctx, text) + .char(':') + .space() + .output(ctx, data_type); + } + FunctionArgument::Error => { + output.error(span); + } + } + } +} + +impl TextOutput for CompilerFlag { + fn output(&self, _span: &Span, output: &mut Output, _ctx: &mut FmtContext) { + output.text(format!("#[{self}]")); + } +} + +impl TextOutput for String { + fn output(&self, _span: &Span, output: &mut Output, _ctx: &mut FmtContext) { + output.text(self.clone()); + } +} + +impl TextOutput for IterLoopVars { + fn output(&self, span: &Span, output: &mut Output, _ctx: &mut FmtContext) { + output.span(span); + } +} + +impl TextOutput for Block { + fn output(&self, span: &Span, output: &mut Output, ctx: &mut FmtContext) { + match self { + Block::Block(modifiers, statements) => { + output.char('{').increase_indentation().end_newline(); + + for modifier in modifiers { + output.output(ctx, modifier).end_space(); + } + for statement in statements { + output.output(ctx, statement).end_newline(); + } + + output + .remove_newline() + .decrease_indentation() + .newline() + .char('}'); + } + Block::Error => output.end_span(span), + } + } +} + +impl TextOutput for IfChainContent { + fn output(&self, span: &Span, output: &mut Output, _ctx: &mut FmtContext) { + output.span(span); + } +} + +impl TextOutput for ElseCondition { + fn output(&self, span: &Span, output: &mut Output, _ctx: &mut FmtContext) { + output.span(span); + } +} + +impl TextOutput for Comment { + fn output(&self, span: &Span, output: &mut Output, _ctx: &mut FmtContext) { + match self { + Comment::Comment(comment) => { + output.end_comment(CommentVariant::Regular, comment.as_str(), span) + } + Comment::DocString(doc_comment) => { + output.end_comment(CommentVariant::Doc, doc_comment.as_str(), span) + } + } + } +} + +impl TextOutput for IfCondition { + fn output(&self, span: &Span, output: &mut Output, ctx: &mut FmtContext) { + match self { + IfCondition::IfCondition(condition, block) => { + output.output(ctx, condition).space().end_output(ctx, block) + } + IfCondition::InlineIfCondition(condition, statement) => { + output + .output(ctx, condition) + .char(':') + .space() + .output(ctx, statement); + } + IfCondition::Comment(comment) => output.end_output(ctx, comment), + IfCondition::Error => output.error(span), + } + } +} + +impl TextOutput for VariableInitType { + fn output(&self, span: &Span, output: &mut Output, ctx: &mut FmtContext) { + match self { + VariableInitType::Expression(expr) => output.end_output(ctx, expr), + VariableInitType::DataType(r#type) => output.end_output(ctx, r#type), + VariableInitType::Error => output.error(span), + } + } +} + +impl TextOutput for FailureHandler { + fn output(&self, _span: &Span, output: &mut Output, ctx: &mut FmtContext) { + match self { + FailureHandler::Propagate => { + output.char('?'); + } + FailureHandler::Handle(failed, statements) => { + output.output(ctx, failed); + output.space(); + output.char('{'); + output.increase_indentation(); + output.newline(); + + for statement in statements { + output.output(ctx, statement); + output.newline(); + } + + output.remove_newline(); + output.decrease_indentation(); + output.newline(); + + output.char('}'); + } + } + } +} + +impl TextOutput for CommandModifier { + fn output(&self, _span: &Span, output: &mut Output, _ctx: &mut FmtContext) { + match self { + CommandModifier::Unsafe => output.end_text("unsafe"), + CommandModifier::Trust => output.end_text("trust"), + CommandModifier::Silent => output.end_text("silent"), + CommandModifier::Sudo => output.end_text("sudo"), + } + } +} + +impl TextOutput for InterpolatedCommand { + fn output(&self, _span: &Span, output: &mut Output, ctx: &mut FmtContext) { + match self { + InterpolatedCommand::Escape(escape) => output + .debug_point("InterpolatedCommand escape") + .end_text(escape.as_str()), + InterpolatedCommand::CommandOption(option) => output.end_text(option.as_str()), + InterpolatedCommand::Expression(expr) => output.end_output(ctx, expr), + InterpolatedCommand::Text(text) => output.end_text(text.as_str()), + } + } +} + +impl TextOutput for f32 {} +impl TextOutput for bool {} +impl TextOutput for DataType {} diff --git a/crates/formatter/src/alpha040/statement.rs b/crates/formatter/src/alpha040/statement.rs new file mode 100644 index 0000000..61c840b --- /dev/null +++ b/crates/formatter/src/alpha040/statement.rs @@ -0,0 +1,152 @@ +use crate::{ + SpanTextOutput, TextOutput, + alpha040::Gen, + format::{FmtContext, Output}, +}; +use amber_grammar::{Span, alpha040::Statement}; + +impl TextOutput for Statement { + fn output(&self, span: &Span, output: &mut Output, ctx: &mut FmtContext) { + fn shorthand( + output: &mut Output, + ctx: &mut FmtContext, + variable: &impl SpanTextOutput, + separator: &str, + expression: &impl SpanTextOutput, + ) { + output + .output(ctx, variable) + .space() + .text(separator) + .space() + .output(ctx, expression); + } + + match self { + Statement::Expression(expression) => { + output.output(ctx, expression); + } + Statement::VariableInit(keyword, name, init) => { + output + .output(ctx, keyword) + .space() + .output(ctx, name) + .space() + .char('=') + .space() + .output(ctx, init); + } + Statement::ConstInit(keyword, name, init) => { + output + .output(ctx, keyword) + .space() + .output(ctx, name) + .space() + .char('=') + .space() + .output(ctx, init); + } + Statement::VariableSet(name, new_value) => { + output + .output(ctx, name) + .space() + .char('=') + .space() + .output(ctx, new_value); + } + Statement::IfCondition(r#if, condition, comments, else_condition) => { + output.output(ctx, r#if).space().output(ctx, condition); + + if let Some(comment) = comments.first() { + ctx.allow_newline(output, condition.1.end..=comment.1.start); + output.output(ctx, comment); + } + + for comment in comments.iter().skip(1) { + output.output(ctx, comment); + } + + if let Some(else_condition) = else_condition { + output.space().output(ctx, else_condition); + } + } + Statement::IfChain(r#if, items) => { + output + .output(ctx, r#if) + .space() + .char('{') + .increase_indentation(); + + for ele in items { + output.newline().output(ctx, ele); + } + + output.decrease_indentation(); + if items.len() > 0 { + output.newline(); + } + output.char('}'); + } + Statement::ShorthandAdd(variable, expr) => shorthand(output, ctx, variable, "+=", expr), + Statement::ShorthandSub(variable, expr) => shorthand(output, ctx, variable, "-=", expr), + Statement::ShorthandMul(variable, expr) => shorthand(output, ctx, variable, "*=", expr), + Statement::ShorthandDiv(variable, expr) => shorthand(output, ctx, variable, "/=", expr), + Statement::ShorthandModulo(variable, expr) => { + shorthand(output, ctx, variable, "%=", expr) + } + Statement::InfiniteLoop(r#loop, block) => { + output.output(ctx, r#loop).space().output(ctx, block); + } + Statement::IterLoop(r#for, element, r#in, expr, block) => { + output + .output(ctx, r#for) + .space() + .output(ctx, element) + .space() + .output(ctx, r#in) + .space() + .output(ctx, expr) + .space() + .end_output(ctx, block); + } + Statement::Break => output.end_text("break"), + Statement::Continue => output.end_text("continue"), + Statement::Return(r#return, expr) => { + output.end_output(ctx, r#return); + + if let Some(expr) = expr { + output.space().end_output(ctx, expr); + } + } + Statement::Fail(fail, expr) => { + output.end_output(ctx, fail); + + if let Some(expr) = expr { + output.space().end_output(ctx, expr); + } + } + Statement::Echo(echo, text) => output.output(ctx, echo).space().end_output(ctx, text), + Statement::Cd(cd, text) => output.output(ctx, cd).space().end_output(ctx, text), + Statement::MoveFiles(modifiers, mv, source, destination, failure_handler) => { + for modifier in modifiers { + output.output(ctx, modifier).space(); + } + + output + .output(ctx, mv) + .space() + .output(ctx, source) + .space() + .output(ctx, destination); + + if let Some(failure_handler) = failure_handler { + output.space().output(ctx, failure_handler); + } + } + Statement::Block(block) => output.end_output(ctx, block), + Statement::Comment(comment) => output.end_output(ctx, comment), + Statement::Shebang(shebang) => output.end_text(shebang.as_str()), + Statement::Error => output.error(span), + } + } +} diff --git a/crates/formatter/src/format.rs b/crates/formatter/src/format.rs new file mode 100644 index 0000000..be5f575 --- /dev/null +++ b/crates/formatter/src/format.rs @@ -0,0 +1,411 @@ +use super::{INDENT, SpanTextOutput, WHITESPACE_BYTES}; +use crate::{ + TextOutput, + fragments::{CommentVariant, Fragment, Indentation}, +}; +use amber_types::token::Span; +use std::{ops::RangeBounds, panic::Location, string::FromUtf8Error}; + +/// Formats the file. +/// +/// items is the parsed file content. +/// file_content is the raw file content. +pub fn format( + items: &[(AmberStatement, Span)], + file_content: &str, +) -> Result +where + AmberStatement: TextOutput<(AmberStatement, Span)>, +{ + // eprintln!("{items:?}"); + let mut output = Output::new(); + let consecutive_newlines = consecutive_newlines(file_content); + let source_newlines = file_content + .bytes() + .enumerate() + .filter_map(|(index, byte)| if byte == b'\n' { Some(index) } else { None }) + .collect(); + + // eprintln!("{consecutive_newlines:?}"); + let mut ctx = FmtContext { + items, + index: 0, + consecutive_newlines, + source_newlines, + }; + + for item in items { + item.output(&mut output, &mut ctx); + ctx.increment(); + } + + // eprintln!("{output:?}"); + output.format(file_content) +} + +/// Index of newlines in the content only separated by whitepspace (ignoring the first newline in a consecutive sequence). +fn consecutive_newlines(file_content: &str) -> Vec { + // amber-lsp parses by bytes, not by chars + let char_indices: Vec = file_content.bytes().collect(); + let mut newlines = Vec::new(); + let mut index = 0; + + while index < char_indices.len() { + let character = char_indices.get(index).unwrap(); + index += 1; + + if *character != b'\n' { + continue; + } + + // Finds subsequent newline chars + while index < char_indices.len() { + let character = char_indices.get(index).unwrap(); + index += 1; + + match *character { + b' ' | b'\t' | b'\r' => { + continue; + } + b'\n' => { + newlines.push(index - 1); + } + _ => { + break; + } + } + } + } + + newlines +} + +/// Contains string fragments of the code ready for finial formatting into a string. +#[derive(Default, Debug)] +pub struct Output { + buffer: Vec, +} + +/// Context about the general structure of the program to use during formatting. +pub struct FmtContext<'a, T> { + /// The parsed top level statements of the program. + items: &'a [T], + /// The location of the statement currently being formatted. + index: usize, + /// The byte index of consecutive newlines in the source file. + consecutive_newlines: Vec, + /// Byte index of newlines in the source file. + source_newlines: Box<[usize]>, +} + +impl<'a, T> FmtContext<'a, T> { + /// Moves the context to the next top level statement. + fn increment(&mut self) { + self.index += 1; + } + + /// Returns the previous top level statement if it exists. + pub fn previous_global(&self) -> Option<&T> { + self.items.get(self.index.checked_sub(1)?) + } + + /// Returns the next top level statement if it exists. + pub fn next_global(&self) -> Option<&T> { + self.items.get(self.index.checked_add(1)?) + } + + pub fn allow_newline>(&self, output: &mut Output, range: R) { + if self + .consecutive_newlines + .iter() + .any(|newline| range.contains(&newline)) + { + output.end_newline(); + } + } + + pub fn source_has_newline>(&self, range: R) -> bool { + self.source_newlines + .iter() + .any(|newline| range.contains(newline)) + } +} + +#[derive(thiserror::Error, Debug)] +pub enum FormattingError { + /// The span does not exist within the source file. + #[error("Invalid span. Starts: {start}; Ends: {end}")] + SpanDoesntExist { start: usize, end: usize }, + /// The span cannot be converted into UTF8 text. + #[error(transparent)] + InvalidSpan(#[from] FromUtf8Error), +} + +impl Output { + pub fn new() -> Self { + Self { buffer: Vec::new() } + } + + #[track_caller] + pub(crate) fn debug_point(&mut self, info: &str) -> &mut Self { + self.text(format!( + "%%Debug point : {info} : at {}%%", + Location::caller() + )) + } + + pub(crate) fn error(&mut self, span: &Span) { + self.span(span); + } + + pub(crate) fn space(&mut self) -> &mut Self { + self.buffer.push(Fragment::Space); + self + } + + pub(crate) fn newline(&mut self) -> &mut Self { + self.buffer.push(Fragment::Newline); + self + } + + pub(crate) fn text(&mut self, text: impl Into>) -> &mut Self { + self.buffer.push(Fragment::Text(text.into())); + self + } + + pub(crate) fn comment( + &mut self, + variant: CommentVariant, + text: impl Into>, + span: &Span, + ) -> &mut Self { + self.buffer.push(Fragment::Comment { + variant, + text: text.into(), + start_index: span.start, + }); + self + } + + pub(crate) fn char(&mut self, character: char) -> &mut Self { + self.buffer + .push(Fragment::Text(character.to_string().into_boxed_str())); + self + } + + pub(crate) fn output( + &mut self, + ctx: &mut FmtContext, + output: &TOutput, + ) -> &mut Self + where + TOutput: SpanTextOutput, + { + output.output(self, ctx); + self + } + + pub(crate) fn span(&mut self, span: &Span) -> &mut Self { + self.buffer.push(Fragment::Span(span.into())); + self + } + + pub(crate) fn end_space(&mut self) { + self.buffer.push(Fragment::Space); + } + + pub(crate) fn end_newline(&mut self) { + self.buffer.push(Fragment::Newline); + } + + pub(crate) fn end_text(&mut self, text: impl Into>) { + self.buffer.push(Fragment::Text(text.into())); + } + + pub(crate) fn end_comment( + &mut self, + variant: CommentVariant, + text: impl Into>, + span: &Span, + ) { + self.buffer.push(Fragment::Comment { + variant, + text: text.into(), + start_index: span.start, + }); + } + + pub(crate) fn end_char(&mut self, character: char) { + self.buffer + .push(Fragment::Text(character.to_string().into_boxed_str())); + } + + pub(crate) fn end_output(&mut self, ctx: &mut FmtContext, output: &TOutput) + where + TOutput: SpanTextOutput, + { + output.output(self, ctx); + } + + pub(crate) fn end_span(&mut self, span: &Span) { + self.span(span); + } + + pub(crate) fn increase_indentation(&mut self) -> &mut Self { + self.buffer + .push(Fragment::IndentationChange(Indentation::Increase)); + self + } + + pub(crate) fn decrease_indentation(&mut self) -> &mut Self { + self.buffer + .push(Fragment::IndentationChange(Indentation::Decrease)); + self + } + + /// Removes the last fragment from the buffer if it is a space. + pub(crate) fn remove_space(&mut self) -> &mut Self { + self.buffer.pop_if(|last| matches!(last, Fragment::Space)); + self + } + + /// Removes the last fragment from the buffer if it is a newline. + pub(crate) fn remove_newline(&mut self) -> &mut Self { + self.buffer.pop_if(|last| matches!(last, Fragment::Newline)); + self + } + + /// Removes the any trailing whitespace fragments. + /// + /// Indentation changes are ignored but still preserved. + pub(crate) fn remove_trailing_whitespace(&mut self) -> &mut Self { + let mut indentation = Vec::new(); + + let is_text = |last: &mut Fragment| { + matches!( + last, + Fragment::IndentationChange(..) | Fragment::Space | Fragment::Newline + ) + }; + + while let Some(fragment) = self.buffer.pop_if(is_text) { + if matches!(fragment, Fragment::IndentationChange(..)) { + indentation.push(fragment); + } + } + + self.buffer.append(&mut indentation); + self + } + + fn format(mut self, file_content: &str) -> Result { + self.remove_trailing_whitespace(); + + let mut text = String::new(); + let mut indentation = String::new(); + let mut consecutive_newlines = 0; + + let mut iter = self.buffer.into_iter().peekable(); + while let Some(fragment) = iter.next() { + if let Fragment::Newline = fragment { + consecutive_newlines += 1; + } else { + consecutive_newlines = 0; + } + + match fragment { + Fragment::Space => text.push(' '), + Fragment::Newline => { + if consecutive_newlines > 2 { + continue; + } + text.push('\n'); + text.push_str(&indentation); + } + Fragment::IndentationChange(indent) => match indent { + Indentation::Increase => indentation += INDENT, + Indentation::Decrease => { + indentation.truncate(indentation.len().saturating_sub(INDENT.len())); + } + }, + Fragment::Text(frag_text) => text.push_str(&frag_text), + Fragment::Span(span) => { + let span = file_content + .as_bytes() + .get(span.start_offset()..=span.end_offset()) + .ok_or_else(|| FormattingError::SpanDoesntExist { + start: span.start_offset(), + end: span.end_offset(), + })?; + + let span_text = String::from_utf8(span.to_vec())?; + text.push_str(&span_text); + } + Fragment::ParseError(span) => { + let span = file_content + .as_bytes() + .get(span.start_offset()..=span.end_offset()) + .ok_or_else(|| FormattingError::SpanDoesntExist { + start: span.start_offset(), + end: span.end_offset(), + })?; + + let span_text = String::from_utf8(span.to_vec())?; + eprintln!("Unable to parse '{span_text}'. Failing back to sourcefile content"); + text.push_str(&span_text); + } + Fragment::Comment { + variant, + text: comment, + start_index: _, + } => { + // Ensure that there is a space between the comment and previous code + if let Some(previous) = text.as_bytes().last() + && !WHITESPACE_BYTES.contains(previous) + { + text.push(' '); + } + + text.push_str(variant.denoted_by()); + text.push(' '); + text.push_str(&comment); + + if let Some(Fragment::Comment { variant, .. }) = iter.peek() + && *variant == CommentVariant::Doc + && matches!(variant, CommentVariant::Doc) + { + text.push('\n'); + } + } + } + } + + Ok(text) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn single_newline() { + let newlines = "Some\nText\nWith\n\nNewlines"; + let consecutive_newlines = consecutive_newlines(newlines); + assert_eq!(consecutive_newlines.as_slice(), &[15]); + } + + #[test] + fn multiple_newline() { + let newlines = "Some\nText\nWith\n\n\nNewlines\n\nMultiple"; + let consecutive_newlines = consecutive_newlines(newlines); + assert_eq!(consecutive_newlines.as_slice(), &[15, 16, 26]); + } + + #[test] + fn multiple_newline_whitespace() { + let newlines = "Some\nText\nWith\n \n \n Newlines\n\t\nMultiple"; + let consecutive_newlines = consecutive_newlines(newlines); + assert_eq!(consecutive_newlines.as_slice(), &[17, 20, 33]); + } +} diff --git a/crates/formatter/src/fragments.rs b/crates/formatter/src/fragments.rs new file mode 100644 index 0000000..667f3e2 --- /dev/null +++ b/crates/formatter/src/fragments.rs @@ -0,0 +1,79 @@ +use amber_types::token::Span; + +#[derive(Debug)] +pub struct FragmentSpan { + /// Start byte offset into source file. + pub(crate) start_offset: usize, + /// End byte offset into source file. + pub(crate) end_offset: usize, +} + +impl FragmentSpan { + pub fn new(start_offset: usize, end_offset: usize) -> Self { + Self { + start_offset: start_offset.min(end_offset), + end_offset: start_offset.max(end_offset), + } + } + + pub fn end_offset(&self) -> usize { + self.end_offset + } + + pub fn start_offset(&self) -> usize { + self.start_offset + } +} + +impl From<&Span> for FragmentSpan { + fn from(value: &Span) -> Self { + FragmentSpan { + start_offset: value.start, + end_offset: value.end.saturating_sub(1), + } + } +} + +#[derive(Debug)] +pub enum Fragment { + Space, + Newline, + /// Denotes an indentation change. + /// This has no output in its current position, but will change the indentation after every newline. + IndentationChange(Indentation), + Text(Box), + Comment { + /// Dentoes the start of a comment. E.G "//" or "///". + variant: CommentVariant, + text: Box, + /// Byte index of the source file where the comment starts. + start_index: usize, + }, + Span(FragmentSpan), + ParseError(FragmentSpan), +} + +#[derive(Debug)] +pub enum Indentation { + Increase, + Decrease, +} + +/// Comments can either be doc comments or regular comments. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum CommentVariant { + /// Doc comment + Doc, + /// Regular comment + Regular, +} + +impl CommentVariant { + /// Returns the string that this comment variant is denoted by. + pub fn denoted_by(self) -> &'static str { + match self { + CommentVariant::Doc => "///", + CommentVariant::Regular => "//", + } + } +} diff --git a/crates/formatter/src/lib.rs b/crates/formatter/src/lib.rs new file mode 100644 index 0000000..506bbb8 --- /dev/null +++ b/crates/formatter/src/lib.rs @@ -0,0 +1,59 @@ +use crate::format::{FmtContext, Output}; +use amber_types::{Spanned, token::Span}; + +mod alpha040; +mod format; +mod fragments; + +pub use format::format; + +/// The content of an indentation. +const INDENT: &str = " "; + +const WHITESPACE: [char; 4] = [' ', '\n', '\r', '\t']; +const WHITESPACE_BYTES: [u8; 4] = { + let mut array = [0; WHITESPACE.len()]; + let mut index = 0; + + while index < WHITESPACE.len() { + array[index] = WHITESPACE[index] as u8; + index += 1; + } + + array +}; + +pub trait SpanTextOutput { + fn output(&self, output: &mut Output, ctx: &mut FmtContext); + + fn span(&self) -> Span; +} + +pub trait TextOutput { + /// Gets the formatted string representation of an AST element. + /// The string representation should be written to the output buffer. + #[allow(unused_variables)] // Not used in default impl. + fn output(&self, span: &Span, output: &mut Output, ctx: &mut FmtContext) { + output.span(span); + } +} + +impl> SpanTextOutput for Spanned { + fn output(&self, output: &mut Output, ctx: &mut FmtContext) { + self.0.output(&self.1, output, ctx); + } + + fn span(&self) -> Span { + self.1 + } +} + +impl> SpanTextOutput for Box> { + fn output(&self, output: &mut Output, ctx: &mut FmtContext) { + self.0.output(&self.1, output, ctx); + } + + fn span(&self) -> Span { + self.1 + } +} diff --git a/crates/formatter/src/main.rs b/crates/formatter/src/main.rs new file mode 100644 index 0000000..e798387 --- /dev/null +++ b/crates/formatter/src/main.rs @@ -0,0 +1,32 @@ +use amber_grammar::{Grammar, LSPAnalysis as _, alpha040::AmberCompiler}; + +fn main() { + let Some(file_path) = std::env::args().skip(1).next() else { + eprintln!("No files to format"); + return; + }; + + let Ok(data) = std::fs::read_to_string(&file_path) + .inspect_err(|err| eprintln!("Unable to read file '{file_path}' err: {err}")) + else { + return; + }; + + let amber_compiler = AmberCompiler::new(); + let tokenize = amber_compiler.tokenize(&data); + let parse = amber_compiler.parse(&tokenize); + + match parse.ast { + Grammar::Alpha034(_items) => todo!(), + Grammar::Alpha035(_items) => todo!(), + Grammar::Alpha040(items) => { + if let Some(items) = items { + { + let format = amber_fmt::format(&items, &data).expect("Able to parse"); + println!("{format}"); + } + } + } + Grammar::Alpha050(_items) => todo!(), + } +} diff --git a/crates/formatter/tests/alpha040.rs b/crates/formatter/tests/alpha040.rs new file mode 100644 index 0000000..b68bd76 --- /dev/null +++ b/crates/formatter/tests/alpha040.rs @@ -0,0 +1,159 @@ +use amber_fmt::format; +use amber_grammar::{ + Grammar, LSPAnalysis as _, Span, + alpha040::{AmberCompiler, GlobalStatement}, +}; + +/// Parse the input string through the amber compiler. +fn parse(data: &str) -> Vec<(GlobalStatement, Span)> { + let amber_compiler = AmberCompiler::new(); + let tokenize = amber_compiler.tokenize(&data); + let parse = amber_compiler.parse(&tokenize); + + let Grammar::Alpha040(Some(items)) = parse.ast else { + panic!("Cannot parse test text"); + }; + + items +} + +/// Asserts that the input string matches the output string after formatting. +fn test_format(input: &str, output: &str) { + let items = parse(input); + let formatted = format(&items, input).expect("Able to format amber file"); + eprintln!("Formatted:\n{}\nOutput:\n{}", formatted, output); + assert_eq!(formatted, output); +} + +#[test] +fn import_allow_newlines() { + test_format( + r#"import { function } from "std/array" +import { function } from "std/array" + +import { function } from "std/array" + + +import { large } from "std/array""#, + r#"import { function } from "std/array" +import { function } from "std/array" + +import { function } from "std/array" + +import { large } from "std/array""#, + ); +} + +#[test] +fn imports_singleline() { + let input = r#"import { function } from "std/array" import { function } from "std/array" import { function } from "std/array""#; + let output = r#"import { function } from "std/array" +import { function } from "std/array" +import { function } from "std/array""#; + + test_format(input, output); +} + +#[test] +fn imports_whitespace() { + test_format( + r#"import { function } from "std/array" "#, + r#"import { function } from "std/array""#, + ); +} + +#[test] +fn imports_newlines() { + test_format( + r#"import +{ +function +} +from +"std/array" +"#, + r#"import { function } from "std/array""#, + ); +} + +#[test] +fn imports_multifunction() { + test_format( + r#"import { function,other } from "std/array""#, + r#"import { function, other } from "std/array""#, + ); +} + +#[test] +fn top_level() { + test_format( + r#"echo "abc" echo "123""#, + r#"echo "abc" +echo "123""#, + ); +} + +#[test] +fn top_level_newlines() { + test_format( + r#"echo "abc" +echo "123" + +echo "123" + + +echo "large" +"#, + r#"echo "abc" +echo "123" + +echo "123" + +echo "large""#, + ); +} + +#[test] +fn if_spacing() { + test_format( + r#"if true { + echo "a" +}"#, + r#"if true { + echo "a" +}"#, + ); +} + +#[test] +fn newline_around_funcs() { + test_format( + concat!( + "let a = 1\n", + "fun some() {\n let a = 2\n}\n", + "let a = 3\n", + "main {\n let a = 4\n}\n", + "let a = 5", + ), + concat!( + "let a = 1\n\n", + "fun some() {\n let a = 2\n}\n\n", + "let a = 3\n\n", + "main {\n let a = 4\n}\n\n", + "let a = 5", + ), + ); +} + +#[test] +fn return_type_spacing_func() { + test_format("fun some() :Num{\n 2\n}", "fun some(): Num {\n 2\n}"); +} + +#[test] +fn function_args() { + test_format( + "fun some(arg1,arg2:Num,arg3=10,arg4:Num=10) :Num{\n 2\n}", + "fun some(arg1, arg2: Num, arg3 = 10, arg4: Num = 10): Num {\n 2\n}", + ); +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..8d6c6a0 --- /dev/null +++ b/flake.lock @@ -0,0 +1,96 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1759036355, + "narHash": "sha256-0m27AKv6ka+q270dw48KflE0LwQYrO7Fm4/2//KCVWg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "e9f00bd893984bc8ce46c895c3bf7cac95331127", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1744536153, + "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1759199574, + "narHash": "sha256-w24RYly3VSVKp98rVfCI1nFYfQ0VoWmShtKPCbXgK6A=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "381776b12d0d125edd7c1930c2041a1471e586c0", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..39d3adb --- /dev/null +++ b/flake.nix @@ -0,0 +1,39 @@ +{ + description = "Environment for amber-lsp."; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + rust-overlay.url = "github:oxalica/rust-overlay"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = + { + nixpkgs, + rust-overlay, + flake-utils, + ... + }: + flake-utils.lib.eachDefaultSystem ( + system: + let + overlays = [ (import rust-overlay) ]; + pkgs = import nixpkgs { inherit system overlays; }; + + rust-build = pkgs.rust-bin.stable.latest.default.override { + extensions = [ "rust-src" ]; + }; + in + { + devShells.default = + with pkgs; + mkShell { + buildInputs = [ + rust-build + bacon + amber-lang + ]; + }; + } + ); +}