diff --git a/.editorconfig b/.editorconfig index 959a8c0d..1821765b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -8,6 +8,6 @@ insert_final_newline = true indent_style = space indent_size = 4 -[*.{json,nix,scm}] +[*.{json,nix,scm,scad}] indent_style = space indent_size = 2 diff --git a/CHANGELOG.md b/CHANGELOG.md index ee0efa53..4f92a786 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ This name should be decided amongst the team before the release. - [#786](https://github.com/tweag/topiary/pull/786) Re-introduce tests to check that all of the language queries are useful. - [#747](https://github.com/tweag/topiary/pull/747) Added support for specifying paths to prebuilt grammars in Topiary's configuration - [#832](https://github.com/tweag/topiary/pull/832) Added `typos-cli` to workspace `Cargo.toml` for spellchecking @mkatychev +- [#845](https://github.com/tweag/topiary/pull/747) Added support for OpenSCAD, @mkatychev - [#851](https://github.com/tweag/topiary/pull/851) Added support for following symlinks when specifying input files for formatting ### Changed diff --git a/README.md b/README.md index 42fe614e..97ec3fc4 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,7 @@ external contributors. They are built in, by default, so are exposed in the same way as supported languages. * [CSS] by @lavigneer +* [OpenSCAD] by @mkatychev #### Experimental @@ -1558,7 +1559,7 @@ The two following steps are enough to jumpstart the formatting of a new language ```nickel c = { extensions = ["c", "h"], - grammar = { + grammar.source.git = { git = "https://github.com/tree-sitter/tree-sitter-c.git", rev = "6c7f459ddc0bcf78b615d3a3f4e8fed87b8b3b1b", }, @@ -1841,6 +1842,7 @@ of choice open in another. [ocamlformat]: https://github.com/ocaml-ppx/ocamlformat [ocamllex]: https://v2.ocaml.org/manual/lexyacc.html [ocp-indent]: https://www.typerex.org/ocp-indent.html +[openscad]: https://en.wikipedia.org/wiki/OpenSCAD [ormolu]: https://github.com/tweag/ormolu [prettier]: https://prettier.io/ [rust]: https://www.rust-lang.org diff --git a/bin/update-wasm-grammars.sh b/bin/update-wasm-grammars.sh index e02a953e..23f0ad57 100755 --- a/bin/update-wasm-grammars.sh +++ b/bin/update-wasm-grammars.sh @@ -153,7 +153,20 @@ css() { echo -e "${GREEN}CSS: Done${NC}" } +openscad() { + echo -e "${BLUE}OpenSCAD: Fetching${NC}" + REPO=$(repo_for_language "openscad") + git clone "${REPO}" "${WORKDIR}/tree-sitter-openscad" &> /dev/null + REV=$(ref_for_language "opescad") + pushd "${WORKDIR}/tree-sitter-openscad" &> /dev/null + git checkout "$REV" &> /dev/null + popd &> /dev/null + echo -e "${ORANGE}OpenSCAD: Building${NC}" + tree-sitter build --wasm "${WORKDIR}/tree-sitter-openscad" + echo -e "${GREEN}OpenSCAD: Done${NC}" +} + -(trap 'kill 0' SIGINT; json & nickel & ocaml & ocamllex & bash & rust & toml & tree-sitter-query & css & wait) +(trap 'kill 0' SIGINT; json & nickel & ocaml & ocamllex & bash & rust & toml & tree-sitter-query & css & openscad & wait) echo -e "${GREEN}Done! All grammars have been updated${NC}" diff --git a/default.nix b/default.nix index 32f8af3f..4a0ac495 100644 --- a/default.nix +++ b/default.nix @@ -4,11 +4,13 @@ , crane , rust-overlay , craneLib -# tree-sitter-Nickel is packaged in Nixpkgs, but it's an older version at the +# tree-sitter-nickel is packaged in Nixpkgs, but it's an older version at the # time of writing. Since updating it seems non trivial, and we need Topiary to # be compatible with Nickel urgently (it is currently blocking for the CI), we # use the tree-sitter-nickel flake directly. , tree-sitter-nickel +# tree-sitter-openscad is not in nixpkgs so use flake directly +, tree-sitter-openscad }: let inherit (pkgs.lib) fileset; @@ -120,7 +122,7 @@ in preConfigurePhases = pkgs.lib.optional nixSupport "useNixConfiguration"; - # ocamllex is not (yet) packaged in nixpkgs + # ocamllex is not (yet) packaged in nixpkgs: # ocamllex="${pkgs.tree-sitter-grammars.tree-sitter-ocamllex}/parser" \ useNixConfiguration = '' bash="${pkgs.tree-sitter-grammars.tree-sitter-bash}/parser" \ @@ -129,6 +131,7 @@ in nickel="${tree-sitter-nickel}/parser" \ ocaml="${pkgs.tree-sitter-grammars.tree-sitter-ocaml}/parser" \ ocaml_interface="${pkgs.tree-sitter-grammars.tree-sitter-ocaml-interface}/parser" \ + openscad="${tree-sitter-openscad}/parser" \ rust="${pkgs.tree-sitter-grammars.tree-sitter-rust}/parser" \ toml="${pkgs.tree-sitter-grammars.tree-sitter-toml}/parser" \ tree_sitter_query="${pkgs.tree-sitter-grammars.tree-sitter-query}/parser" \ diff --git a/flake.lock b/flake.lock index 73750b59..69863ee8 100644 --- a/flake.lock +++ b/flake.lock @@ -69,7 +69,8 @@ "crane": "crane", "nixpkgs": "nixpkgs", "rust-overlay": "rust-overlay", - "tree-sitter-nickel": "tree-sitter-nickel" + "tree-sitter-nickel": "tree-sitter-nickel", + "tree-sitter-openscad": "tree-sitter-openscad" } }, "rust-overlay": { @@ -109,6 +110,26 @@ "repo": "tree-sitter-nickel", "type": "github" } + }, + "tree-sitter-openscad": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1737584789, + "narHash": "sha256-j/DordcjrrVZ9ZI2p46z4TULyf2A+4mNjcM9btowyQU=", + "owner": "mkatychev", + "repo": "tree-sitter-openscad", + "rev": "270e5ff749edfacc84a6e4a434abd4e8b0f70bbe", + "type": "github" + }, + "original": { + "owner": "mkatychev", + "repo": "tree-sitter-openscad", + "type": "github" + } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 6bd30615..563ac099 100644 --- a/flake.nix +++ b/flake.nix @@ -26,6 +26,11 @@ url = "github:nickel-lang/tree-sitter-nickel"; inputs.nixpkgs.follows = "nixpkgs"; }; + + tree-sitter-openscad = { + url = "github:mkatychev/tree-sitter-openscad"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; outputs = { self, nixpkgs, ... }@inputs: @@ -44,7 +49,7 @@ topiaryPkgs = pkgs.callPackage ./default.nix { inherit (inputs) advisory-db crane rust-overlay; - inherit (pkgs.tree-sitter-grammars) tree-sitter-nickel; + inherit (pkgs.tree-sitter-grammars) tree-sitter-nickel tree-sitter-openscad; craneLib = inputs.crane.mkLib pkgs; }; @@ -64,6 +69,7 @@ # Nickel *should* have an overlay like this already tree-sitter-grammars = prev.tree-sitter-grammars // { tree-sitter-nickel = inputs.tree-sitter-nickel.packages.${prev.system}.default; + tree-sitter-openscad = inputs.tree-sitter-openscad.packages.${prev.system}.default; }; }; diff --git a/topiary-cli/Cargo.toml b/topiary-cli/Cargo.toml index ad4c8515..54704c6b 100644 --- a/topiary-cli/Cargo.toml +++ b/topiary-cli/Cargo.toml @@ -66,12 +66,13 @@ default = [ "ocaml_interface", "ocamllex", "toml", - "tree_sitter_query" + "tree_sitter_query", ] # Included by default contributed = [ - "css" + "css", + "openscad", ] # Excluded by default @@ -86,6 +87,7 @@ nickel = ["topiary-config/nickel", "topiary-queries/nickel"] ocaml = ["topiary-config/ocaml", "topiary-queries/ocaml"] ocaml_interface = ["topiary-config/ocaml_interface", "topiary-queries/ocaml_interface"] ocamllex = ["topiary-config/ocamllex", "topiary-queries/ocamllex"] +openscad = ["topiary-config/openscad", "topiary-queries/openscad"] rust = ["topiary-config/rust", "topiary-queries/rust"] toml = ["topiary-config/toml", "topiary-queries/toml"] tree_sitter_query = ["topiary-config/tree_sitter_query", "topiary-queries/tree_sitter_query"] diff --git a/topiary-cli/src/io.rs b/topiary-cli/src/io.rs index 4688666c..3480c640 100644 --- a/topiary-cli/src/io.rs +++ b/topiary-cli/src/io.rs @@ -349,6 +349,9 @@ where #[cfg(feature = "ocamllex")] "ocamllex" => Ok(topiary_queries::ocamllex().into()), + #[cfg(feature = "openscad")] + "openscad" => Ok(topiary_queries::openscad().into()), + #[cfg(feature = "rust")] "rust" => Ok(topiary_queries::rust().into()), diff --git a/topiary-cli/tests/sample-tester.rs b/topiary-cli/tests/sample-tester.rs index ebb69def..05766393 100644 --- a/topiary-cli/tests/sample-tester.rs +++ b/topiary-cli/tests/sample-tester.rs @@ -87,6 +87,9 @@ fn input_output_tester() { #[cfg(feature = "ocamllex")] io_test("ocamllex.mll"); + #[cfg(feature = "openscad")] + io_test("openscad.scad"); + #[cfg(feature = "rust")] io_test("rust.rs"); diff --git a/topiary-cli/tests/samples/expected/openscad.scad b/topiary-cli/tests/samples/expected/openscad.scad new file mode 100644 index 00000000..1bb94d90 --- /dev/null +++ b/topiary-cli/tests/samples/expected/openscad.scad @@ -0,0 +1,220 @@ +// ================================================================================ +// Variables/Imports +// ================================================================================ +include ; +use ; +include +use +// variables +rr = a_vector[2]; // member of vector +range1 = [-1.5:0.5:3]; // for() loop range +xx = [0:5]; // alternate for() loop range +$fn = 360; // special variable +E = 2.71828182845904523536028747135266249775724709369995; // constant +cond_var = "is_true" ? true : false; + +// ================================================================================ +// Functions +// ================================================================================ +function line(point1, point2, width = 1) = + + let (angle = 90 - atan( (point2[1] - point1[1]) / (point2[0] - point1[0]) )) + + let (offset_x = 0.5 * width * cos(angle), offset_y = 0.5 * width * sin(angle)) + + let (offset1 = [-offset_x, offset_y], offset2 = [offset_x, -offset_y]) + + // [P1a, P2a, P2b, P1b] + [point1 + offset1, point2 + offset1, point2 + offset2, point1 + offset2]; + +eager = (function(z) identity)(4); + +// ================================================================================ +// Transformations +// ================================================================================ +cylinder(); +cylinder(d=5, h=100); +rotate([90, 0, 0]) + cylinder(); +translate([1, 0, 0]) { + difference() { + translate([0, 1, 0]) + translate([1, 0, 0]) rotate([0, 90, 0]) + cylinder(); cube(); + } +} + +// ================================================================================ +// Nested Items +// ================================================================================ +module big_module() { + function inner_function() = undef; + module inner_module() cube(); +} + +module extern_module() include + +// ================================================================================ +// Control Flow +// ================================================================================ +for (i = [10:50]) { + let (angle = i * 360 / 20, r = i * 2, distance = r * 5) { + rotate(angle, [1, 0, 0]) translate([0, distance, 0]) + sphere(r=r); + } +} + +// newline indent propagates from innermost if_block +if ($preview) + if (true) + foo(); + else if (true) + if (false) + foo(false); + else + translate([2, 0, 0]) foo(); + else + bar(); + +// format propagates from first union_block +if (true) { +} else if (fn(true)) { + foo(); +} else if (false) { + bar(); +} else { + baz(); +} + +for (i = [1:2:7]) { + let (x = i ^ 2, y = x - 1) { + translate([x, y, 0]) sphere(r=i); + } +} + +intersection_for (i = [1, 2, 3]) { + if (i > 1) { + translate([0, i, 0]) cube(); + } else { + translate([0, i, 0]) cube(); + } +} + +// ================================================================================ +// Comments +// ================================================================================ + +/* ignored [Customizer Group] ignored */ +/* Multiline +comment +here +*/ +my_parameter = /*inline block*/ 5; + +function math(x) = + /*do math stuff*/ x + 2 // done with math +; + +module my_cylinder() { + // here we create a cylinder + cylinder(); /* done ! */ + cube(); +} + +// ================================================================================ +// Modifiers +// ================================================================================ +!cylinder(); +*linear_extrude(4) text("Hello"); +rotate([0, 90, 0]) #cylinder(); +%cube(); +// multi modifier +translate(1) #!cube(); +rotate([90]) %translate() + #cube(); + +// ================================================================================ +// Assertions/Echoes +// ================================================================================ +assert(); +assert(10 < 20) cube(); +for (y = [3:5]) + assert(assert() y < x, "message") + cylinder(); +assert(true) assert() cylinder(); +val = + assert(true, "strut must be positive") + assert(true, "frame must be nonnegative") + undef; + +function foo() = + echo("this can precede an expression") true; + +fn = function(x) echo("this is x") x; +echo(fn ? "truthy" : "falsey"); +echo(function(y) y ? "first" : "second"); + +// ================================================================================ +// Lists/Ternaries +// ================================================================================ +list1 = [ + 1, + 2, + 3, +]; +my_fn = fn1( + [ + 1, + 2, + 3, + ], + true, +); + +function affine3d_rot_from_to(from, to) = + assert(is_vector(from)) + assert(is_vector(to)) + assert(len(from) == len(to)) + let ( + from = unit(point3d(from)), + to = unit(point3d(to)), + ) approx(from, to) ? affine3d_identity() + : from.z == 0 && to.z == 0 ? affine3d_zrot(v_theta(point2d(to)) - v_theta(point2d(from))) + : let ( + u = vector_axis(from, to), + ang = vector_angle(from, to), + c = cos(ang), + c2 = 1 - c, + s = sin(ang), + // double indent a list preceded by list expression + ) [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1], + ]; + +// Prettier style ternaries https://prettier.io/blog/2023/11/13/curious-ternaries +x = + foo() ? bar() + : baz() ? + qux() + : true; + +// ================================================================================ +// Comprehensions +// ================================================================================ +conditionless = [for (x = [1:10]) x]; +conditioned = [for (x = [1:10]) if ($preview) x]; +ifelse = [for (x = [1:10]) if ($preview) x else ln(x)]; +if_for_ifelse = [for (x = 0) if (x < 0) for (y = 2) if (y == 2) y else x]; +complex_condition = [ + for (x = [1:10]) if (x % 2 == 0) x else if (x < 5) x - 1 else 0, +]; +spliced = [for (x = [1:10]) x, for (y = [1, 2, 3]) y, for (z = [4, 5, 6]) z]; +nested = [for (x = [1:10]) for (y = [1, 2, 3]) for (z = [4, 5, 6]) x * y * z]; +grouped = [if (x < 7) (for (y = [1:10]) if (y > x) y) else x]; +let_each = [for (i = [0:1]) let (a = 90) each arc(angle=a)]; +let_for = [let (i = [0:1]) for (i = i) let (a = 90) each arc(angle=a)]; +let_if = [for (i = [0:1]) let (a = 360) if (is_def(isect)) isect]; +fn_list = [each function() 10]; diff --git a/topiary-cli/tests/samples/input/openscad.scad b/topiary-cli/tests/samples/input/openscad.scad new file mode 100644 index 00000000..fdf65f85 --- /dev/null +++ b/topiary-cli/tests/samples/input/openscad.scad @@ -0,0 +1,187 @@ +// ================================================================================ +// Variables/Imports +// ================================================================================ +include ; +use ; include use +// variables +rr = a_vector[2]; // member of vector +range1 = [-1.5:0.5:3]; // for() loop range +xx = [0:5]; // alternate for() loop range +$fn = 360; // special variable +E = 2.71828182845904523536028747135266249775724709369995; // constant +cond_var = "is_true" ? true : false; + +// ================================================================================ +// Functions +// ================================================================================ +function line(point1, point2, width = 1) = + + let (angle = 90 - atan((point2[1] - point1[1]) / (point2[0] - point1[0]))) + + let (offset_x = 0.5 * width * cos(angle), offset_y = 0.5 * width * sin(angle)) + + let (offset1 = [ -offset_x, offset_y], offset2 = [offset_x, -offset_y]) + + // [P1a, P2a, P2b, P1b] + [point1 + offset1, point2 + offset1, point2 + offset2, point1 + offset2]; + +eager = (function(z) identity)(4); + +// ================================================================================ +// Transformations +// ================================================================================ +cylinder(); +cylinder( d = 5, h=100,); +rotate([90, 0, 0]) +cylinder(); +translate([1, 0, 0]) { + difference() { translate([0, 1, 0]) + translate([1, 0, 0]) rotate([0, 90, 0]) + cylinder(); cube(); } } + +// ================================================================================ +// Nested Items +// ================================================================================ +module big_module() { function inner_function() = undef; module inner_module() cube(); +} + +module extern_module() include + +// ================================================================================ +// Control Flow +// ================================================================================ +for (i = [10:50]) { let (angle = i*360/20, r= i*2, distance = r*5) { + rotate(angle, [1, 0, 0]) translate([0, distance, 0]) + sphere(r = r); } +} + +// newline indent propagates from innermost if_block +if ($preview) if(true) foo(); else if(true) if + (false) foo(false); else translate([2,0,0]) foo(); else bar(); + +// format propagates from first union_block +if (true) { + } else if (fn(true)) { foo();} else if (false) { bar();} else { baz(); } + + +for(i = [1:2:7]) { let (x = i ^ 2,y = x - 1) { + translate([x,y,0]) sphere(r=i); } } + +intersection_for(i = [1,2,3]) { if (i > 1) { + translate([0,i,0]) cube(); } else { translate([0,i,0]) cube(); } } + +// ================================================================================ +// Comments +// ================================================================================ + +/* ignored [Customizer Group] ignored */ +/* Multiline +comment +here +*/ +my_parameter = /*inline block*/ 5; + +function math(x) = /*do math stuff*/ x + 2 // done with math +; + +module my_cylinder() { + // here we create a cylinder + cylinder(); /* done ! */ + cube(); +} + +// ================================================================================ +// Modifiers +// ================================================================================ +!cylinder(); +* linear_extrude(4) text("Hello"); +rotate([0,90,0])# cylinder(); +%cube(); +// multi modifier +translate(1) # ! cube(); +rotate([90]) % +translate() +# +cube(); + +// ================================================================================ +// Assertions/Echoes +// ================================================================================ +assert(); +assert(10 < 20) cube(); +for(y = [3:5]) assert(assert() y < x, "message") + cylinder(); +assert(true) assert() cylinder(); +val = assert(true, "strut must be positive") +assert(true, "frame must be nonnegative") +undef; + +function foo() = +echo("this can precede an expression") true; + + +fn = function(x) echo("this is x") x; +echo(fn ? "truthy" : "falsey"); +echo(function(y) y ? "first" : "second"); + +// ================================================================================ +// Lists/Ternaries +// ================================================================================ + list1 = [ + 1, + 2, 3 + ]; + my_fn = fn1([ + 1, + 2, + 3, + ], + true + ); + +function affine3d_rot_from_to(from, to) = + assert(is_vector(from)) + assert(is_vector(to)) + assert(len(from) == len(to)) + let ( + from = unit(point3d(from)), + to = unit(point3d(to)) + ) approx(from, to) ? affine3d_identity() + : from.z == 0 && to.z == 0 ? affine3d_zrot(v_theta(point2d(to)) - v_theta(point2d(from))) + : let (u = vector_axis(from, to), + ang = vector_angle(from, to), + c = cos(ang), + c2 = 1 - c, + s = sin(ang) + // double indent a list preceded by list expression + ) [ + [1, 0, 0, 0], + [0, 1, 0, 0], + [0, 0, 1, 0], + [0, 0, 0, 1], + ]; + +// Prettier style ternaries https://prettier.io/blog/2023/11/13/curious-ternaries +x = foo() ? bar() + : baz() ? + qux() + : true; + +// ================================================================================ +// Comprehensions +// ================================================================================ +conditionless = [for (x = [1:10]) x]; +conditioned = [for (x = [1:10]) if ($preview) x]; +ifelse = [for (x = [1:10]) if ($preview) x else ln(x)]; +if_for_ifelse = [for (x = 0) if (x < 0) for (y = 2) if (y == 2) y else x]; +complex_condition = [ + for (x = [1:10]) if (x % 2 == 0) x else if (x < 5) x - 1 else 0 +]; +spliced = [for (x = [1:10]) x, for (y = [1, 2, 3]) y, for (z = [4, 5, 6]) z]; +nested = [for (x = [1:10]) for (y = [1, 2, 3]) for (z = [4, 5, 6]) x * y * z]; +grouped = [if (x < 7) (for (y = [1:10]) if (y > x) y) else x]; +let_each = [for(i = [0:1]) let(a=90) each arc(angle=a)]; +let_for = [let (i=[0:1]) for(i = i) let(a=90) each arc(angle=a)]; +let_if = [for(i = [0:1]) let(a=360) if (is_def(isect)) isect]; +fn_list = [each function ()10]; + diff --git a/topiary-config/Cargo.toml b/topiary-config/Cargo.toml index bea1f754..04117aa0 100644 --- a/topiary-config/Cargo.toml +++ b/topiary-config/Cargo.toml @@ -47,6 +47,7 @@ nickel = [] ocaml = [] ocaml_interface = [] ocamllex = [] +openscad = [] rust = [] toml = [] tree_sitter_query = [] @@ -61,6 +62,7 @@ all = [ "ocaml", "ocaml_interface", "ocamllex", + "openscad", "rust", "toml", "tree_sitter_query", diff --git a/topiary-config/languages.ncl b/topiary-config/languages.ncl index 7a29814b..0ca74f92 100644 --- a/topiary-config/languages.ncl +++ b/topiary-config/languages.ncl @@ -73,6 +73,14 @@ }, }, + openscad = { + extensions = ["scad"], + grammar.source.git = { + git = "https://github.com/mkatychev/tree-sitter-openscad.git", + rev = "270e5ff749edfacc84a6e4a434abd4e8b0f70bbe", + }, + }, + rust = { extensions = ["rs"], indent = " ", # 4 spaces diff --git a/topiary-config/languages_nix.ncl b/topiary-config/languages_nix.ncl index 57c71c26..a4e85bbc 100644 --- a/topiary-config/languages_nix.ncl +++ b/topiary-config/languages_nix.ncl @@ -10,6 +10,11 @@ grammar.source.path = "@css@", }, + openscad = { + extensions = ["scad"], + grammar.source.path = "@openscad@", + }, + json = { extensions = [ "json", diff --git a/topiary-queries/Cargo.toml b/topiary-queries/Cargo.toml index 45969a10..2920f7ea 100644 --- a/topiary-queries/Cargo.toml +++ b/topiary-queries/Cargo.toml @@ -22,6 +22,7 @@ nickel = [] ocaml = [] ocaml_interface = [] ocamllex = [] +openscad = [] rust = [] toml = [] tree_sitter_query = [] diff --git a/topiary-queries/queries/openscad.scm b/topiary-queries/queries/openscad.scm new file mode 100644 index 00000000..c17eba9b --- /dev/null +++ b/topiary-queries/queries/openscad.scm @@ -0,0 +1,385 @@ +; Sometimes we want to indicate that certain parts of our source text should +; not be formatted, but taken as is. We use the leaf capture name to inform the +; tool of this. +[ + (block_comment) + (line_comment) + (string) +] @leaf + +; Allow blank line before +[ + (use_statement) + (intersection_for_block) + (for_block) + (if_block) + (let_block) + (assign_block) + (union_block) + (transform_chain) + (include_statement) + (assert_statement) + (line_comment) + (block_comment) + (function_item) + (module_item) + (expression) + (var_declaration) +] @allow_blank_line_before + +; Keywords + +; Surround spaces +[ + "module" + "let" + "include" + "assign" + "use" + "each" + "else" + "if" + "||" + "&&" + "==" + "!=" + "<" + ">" + "<=" + ">=" + "+" + "-" + "*" + "/" + "%" + "^" + "=" + "?" + ":" + (parenthesized_expression) + (assignments) +] @prepend_space @append_space + +; Colon should have whitespace trimmed in a range delimiter +(range ":" @prepend_antispace @append_antispace) + +; Input softlines before and after all comments. This means that the input +; decides if a comment should have line breaks before or after. A line comment +; always ends with a line break. +[ + (block_comment) + (line_comment) +] @prepend_input_softline + +; Append line breaks. If there is a comment following, we don't add anything, +; because the input softlines and spaces above will already have sorted out the +; formatting. +( + [ + (var_declaration) + (function_item) + (module_item) + (intersection_for_block) + (for_block) + (if_block) + (let_block) + (assign_block) + (union_block) + (use_statement) + (include_statement) + (assert_statement) + ] @append_spaced_softline + . + [ + "else" + (block_comment) + (line_comment) + ]* @do_nothing +) + +(line_comment) @append_hardline + +(block_comment) @multi_line_indent_all + +; Allow line break after block comments +( + (block_comment) + . + _ @prepend_input_softline +) + +; Append softlines, unless followed by comments. +; When binding multiple values in a let block, allow new lines between the bindings. +(list + "[" @append_indent_start @append_empty_softline @append_antispace + "]" @prepend_indent_end @prepend_empty_softline @prepend_antispace +) +; to avoid having a list that directly follows a let_expression look visually unindented, +; add another level of indentation: +; let ( +; u = true, +; )[ +; [1, 0, 0, 0], +; [0, 1, 0, 0], +; [0, 0, 1, 0], +; [0, 0, 0, 1], +; ]; +(let_expression + (list + "[" @append_indent_start + "]" @prepend_indent_end + ) +) + +(range + "[" @append_antispace + "]" @prepend_antispace +) +(list "," @append_spaced_softline . [(block_comment) (line_comment)]* @do_nothing) +(assignments "," @append_spaced_softline . [(block_comment) (line_comment)]* @do_nothing) +(parameters "," @append_spaced_softline . [(block_comment) (line_comment)]* @do_nothing) +(";" @append_spaced_softline . [(block_comment) (line_comment)]* @do_nothing) + +; Never put a space before a comma +("," @prepend_antispace) +(";" @prepend_antispace) + +; Don't insert spaces between the operator and their expression operand +; '-x' v.s. '- x' +(unary_expression _ @append_antispace . (expression)) + +; ================================================================================ +; functions & modules +; ================================================================================ +(function_item "function" @append_space) + +; indent the body of a function +(function_item + (parameters) + . + "=" @append_spaced_softline @append_indent_start + (expression) + ";" @prepend_indent_end +) + +; === +; function literals +; === +(function_lit (parameters) @append_spaced_softline) +(function_call (parenthesized_expression) @append_antispace (arguments)) + +; === +; module_item, module_call, and transform_chain +; === + +; everything except `union_block` after a for/if/else statement should be a spaced_softline +(module_item + body: [ + (for_block) + (intersection_for_block) + (if_block) + (let_block) + (assign_block) + (transform_chain) + (include_statement) + (assert_statement) + ] @prepend_indent_start @append_indent_end +) + +; module calls in a transformation chain will follow each other +; sometimes staying on the same line and sometimes having a linebreak, +; each linebreak typically also starts an indent scope +(transform_chain) @prepend_input_softline +(transform_chain + (modifier)* + (module_call) @append_indent_start + (transform_chain) @append_indent_end +) + +; ================================================================================ +; blocks/expressions/statements +; ================================================================================ + +; Let child nodes handle indentation +(var_declaration . (assignment . (identifier) . "=" @append_input_softline)) + +(assignments) @append_space +(assignments + (#delimiter! ",") + (assignment) @append_delimiter + . + ","? @do_nothing + . + (line_comment)* + . + ")" + . + (#multi_line_only!) +) +(assignments + . + "(" @append_empty_softline @append_indent_start + ")" @prepend_indent_end @prepend_empty_softline + . +) +(assignments "," @delete . ")" . (#single_line_only!)) +(assignments "," @append_spaced_softline) + +(arguments "," @append_input_softline) +(arguments "," @delete . ")" . (#single_line_only!)) +(arguments + . + "(" @append_empty_softline @append_indent_start + ")" @prepend_indent_end @prepend_empty_softline + . +) +(arguments + (#delimiter! ",") + (_) @append_delimiter + . + ","? @do_nothing + . + (line_comment)* + . + ")" + . + (#multi_line_only!) +) + +(parameters "," @append_input_softline) +(parameters "," @delete . ")" . (#single_line_only!)) +(parameters + . + "(" @append_empty_softline @append_indent_start + ")" @prepend_indent_end @prepend_empty_softline + . +) + +(parenthesized_expression + . + "(" @append_empty_softline @append_indent_start + ")" @prepend_indent_end @prepend_empty_softline + . +) +(list "," @delete . "]" . (#single_line_only!)) +(list + (#delimiter! ",") + (_) @append_delimiter + . + ","? @do_nothing + . + "]" + . + (#multi_line_only!) +) + +; differentiate parameter definitions from parameter invocation, +; module/function definitions have param separation while +; module/function calls have none, this space on chained function/module calls +; and provides visual distinction between definitions and calls +(arguments + (assignment + "=" @append_antispace @prepend_antispace + ) +) + +(union_block + . + "{" @append_spaced_softline @append_indent_start @prepend_space + _ + "}" @prepend_spaced_softline @prepend_indent_end + . +) + +; everything except `union_block` after a for/if/else statement should be a spaced_softline +(if_block + (parenthesized_expression) @append_spaced_softline @append_indent_start + . + [ + (for_block) + (intersection_for_block) + (if_block) + (let_block) + (assign_block) + (transform_chain) + (include_statement) + (assert_statement) + ] @append_indent_end @append_spaced_softline +) +( + "else" @append_spaced_softline @append_indent_start + . + [ + (for_block) + (intersection_for_block) + (let_block) + (assign_block) + (transform_chain) + (include_statement) + (assert_statement) + ] @append_indent_end +) @prepend_spaced_softline + +; scope is triggered by the presence of a (union_block) consequce on the intersection_for_block; +; and extends to the "else if" and "else" portions +( + "else"? @do_nothing + (if_block + (#scope_id! "if_union") + ) @prepend_begin_scope @append_end_scope +) + +(if_block + (#scope_id! "if_union") + (_ + . + "{" @append_spaced_scoped_softline + "}" @prepend_spaced_scoped_softline + . + ) +) + +(for_block + (assignments) @append_spaced_softline @append_indent_start + . + [ + (for_block) + (intersection_for_block) + (if_block) + (let_block) + (assign_block) + (transform_chain) + (include_statement) + (assert_statement) + ] @append_indent_end @append_spaced_softline +) +; modifiers +(modifier) @append_antispace + +; echo/assert statements can often be chained, use indentation on assignment +(var_declaration + (assignment + "=" @append_spaced_softline + value: [ + (assert_expression) + (echo_expression) + (ternary_expression) + ] @prepend_indent_start + ) + ";" @prepend_indent_end + . +) + +(assert_expression expression: (_) @prepend_spaced_softline) +(assert_statement statement: (_) @prepend_spaced_softline) +(echo_expression expression: (_) @prepend_spaced_softline) + +; ternary expressions +(ternary_expression + ":" @prepend_spaced_softline +) +; Prettier style ternaries https://prettier.io/blog/2023/11/13/curious-ternaries +(ternary_expression + "?" @append_input_softline @append_indent_start + ":" @prepend_indent_end +) diff --git a/topiary-queries/src/lib.rs b/topiary-queries/src/lib.rs index 20c886e6..c7d3bbd8 100644 --- a/topiary-queries/src/lib.rs +++ b/topiary-queries/src/lib.rs @@ -40,6 +40,12 @@ pub fn ocamllex() -> &'static str { include_str!("../queries/ocamllex.scm") } +/// Returns the Topiary-compatible query file for OpenSCAD. +#[cfg(feature = "openscad")] +pub fn openscad() -> &'static str { + include_str!("../queries/openscad.scm") +} + /// Returns the Topiary-compatible query file for Rust. #[cfg(feature = "rust")] pub fn rust() -> &'static str {