diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 22e3e09d..6e40a277 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -457,7 +457,7 @@ jobs: path: dist/plugins retention-days: "7" build-plugins-maple: - name: "Plugins (maple): caddy, cmake, dockerfile, dot, graphql, hcl, ini, jq, meson, nginx, ninja, nix, query, rego, ron, sql, ssh-config, styx, toml, yaml" + name: "Plugins (maple): caddy, cmake, dockerfile, dot, graphql, hcl, ini, jq, just, meson, nginx, ninja, nix, query, rego, ron, sql, ssh-config, styx, toml, yaml" runs-on: depot-ubuntu-24.04-32 container: "ghcr.io/bearcove/arborium-plugin-builder:latest" needs: @@ -475,10 +475,10 @@ jobs: set -e tar -xf generate-output.tar && rm generate-output.tar shell: bash - - name: Build caddy, cmake, dockerfile, dot, graphql, hcl, ini, jq, meson, nginx, ninja, nix, query, rego, ron, sql, ssh-config, styx, toml, yaml + - name: Build caddy, cmake, dockerfile, dot, graphql, hcl, ini, jq, just, meson, nginx, ninja, nix, query, rego, ron, sql, ssh-config, styx, toml, yaml run: |- set -e - ./xtask/target/release/xtask build caddy cmake dockerfile dot graphql hcl ini jq meson nginx ninja nix query rego ron sql ssh-config styx toml yaml -o dist/plugins + ./xtask/target/release/xtask build caddy cmake dockerfile dot graphql hcl ini jq just meson nginx ninja nix query rego ron sql ssh-config styx toml yaml -o dist/plugins shell: bash - name: Upload plugins artifact uses: actions/upload-artifact@v4 diff --git a/langs/group-maple/just/def/arborium.yaml b/langs/group-maple/just/def/arborium.yaml new file mode 100644 index 00000000..980cb011 --- /dev/null +++ b/langs/group-maple/just/def/arborium.yaml @@ -0,0 +1,19 @@ +repo: https://github.com/casey/tree-sitter-just +commit: 5685543a6e64f66335e25518c9ae8ffa1dae3d01 +license: Apache-2.0 + +grammars: + - id: just + name: Just + tag: build + tier: 1 + has_scanner: true + + inventor: Casey Rodarmor + year: 2016 + description: Just a command runner. + link: https://just.systems/ + + samples: + - path: samples/Justfile + license: MIT OR Apache-2.0 diff --git a/langs/group-maple/just/def/grammar/grammar.js b/langs/group-maple/just/def/grammar/grammar.js new file mode 100644 index 00000000..d75a3f26 --- /dev/null +++ b/langs/group-maple/just/def/grammar/grammar.js @@ -0,0 +1,375 @@ +/** + * @file Justfile grammar for tree-sitter + * @author Anshuman Medhi + * @author Trevor Gross + * @author Amaan Qureshi + * @license Apache-2.0 + */ + +/// +// @ts-check + +const ESCAPE_SEQUENCE = token(/\\([nrt"\\]|(\r?\n))/); +// Flags to `/usr/bin/env`, anything that starts with a dash +const SHEBANG_ENV_FLAG = token(/-\S*/); + +/** + * Creates a rule to match one or more of the rules separated by a comma + * + * @param {RuleOrLiteral} rule + * + * @return {SeqRule} + */ +function comma_sep1(rule) { + return seq(rule, repeat(seq(",", rule))); +} + +/** + * Creates a rule to match an array-like structure filled with `item` + * + * @param {RuleOrLiteral} rule + * + * @return {Rule} + */ +function array(rule) { + const item = field("element", rule); + return field( + "array", + seq( + "[", + optional(field("content", seq(comma_sep1(item), optional(item)))), + "]", + ), + ); +} + +export default grammar({ + name: "just", + + externals: ($) => [ + $._indent, + $._dedent, + $._newline, + $.text, + $.error_recovery, + ], + + // Allow comments, backslash-escaped newlines (with optional trailing whitespace), + // and whitespace anywhere + extras: ($) => [$.comment, /\\(\n|\r\n)\s*/, /\s/], + + inline: ($) => [ + $._string, + $._string_indented, + $._raw_string_indented, + $._expression_recurse, + ], + word: ($) => $.identifier, + + rules: { + // justfile : item* EOF + source_file: ($) => + seq(optional(seq($.shebang, $._newline)), repeat($._item)), + + // item : recipe + // | alias + // | assignment + // | export + // | import + // | module + // | setting + _item: ($) => + choice( + $.recipe, + $.alias, + $.assignment, + $.export, + $.import, + $.module, + $.setting, + ), + + // alias : 'alias' NAME ':=' NAME + alias: ($) => + seq( + repeat($.attribute), + "alias", + field("left", $.identifier), + ":=", + field("right", $.identifier), + ), + // assignment : NAME ':=' expression _eol + assignment: ($) => + seq( + field("left", $.identifier), + ":=", + field("right", $.expression), + $._newline, + ), + + // export : 'export' assignment + export: ($) => seq("export", $.assignment), + + // import : 'import' '?'? string? + import: ($) => seq("import", optional("?"), $.string), + + // module : 'mod' '?'? string? + module: ($) => + seq( + "mod", + optional("?"), + field("name", $.identifier), + optional($.string), + ), + + // setting : 'set' 'dotenv-load' boolean? + // | 'set' 'export' boolean? + // | 'set' 'positional-arguments' boolean? + // | 'set' 'shell' ':=' '[' string (',' string)* ','? ']' + setting: ($) => + choice( + seq( + "set", + field("left", $.identifier), + field( + "right", + optional(seq(":=", choice($.boolean, $.string, array($.string)))), + ), + $._newline, + ), + seq("set", "shell", ":=", field("right", array($.string)), $._newline), + ), + + // boolean : ':=' ('true' | 'false') + boolean: (_) => choice("true", "false"), + + // expression : 'if' condition '{' expression '}' 'else' '{' expression '}' + // | value '/' expression + // | value '+' expression + // | value + expression: ($) => seq(optional("/"), $._expression_inner), + + _expression_inner: ($) => + choice( + $.if_expression, + prec.left(2, seq($._expression_recurse, "+", $._expression_recurse)), + prec.left(1, seq($._expression_recurse, "/", $._expression_recurse)), + $.value, + ), + + // We can't mark `_expression_inner` inline because it causes an infinite + // loop at generation, so we just alias it. + _expression_recurse: ($) => alias($._expression_inner, "expression"), + + if_expression: ($) => + seq( + "if", + $.condition, + field("consequence", $._braced_expr), + repeat(field("alternative", $.else_if_clause)), + optional(field("alternative", $.else_clause)), + ), + + else_if_clause: ($) => seq("else", "if", $.condition, $._braced_expr), + + else_clause: ($) => seq("else", $._braced_expr), + + _braced_expr: ($) => seq("{", field("body", $.expression), "}"), + + // condition : expression '==' expression + // | expression '!=' expression + // | expression '=~' expression + condition: ($) => + choice( + seq($.expression, "==", $.expression), + seq($.expression, "!=", $.expression), + seq($.expression, "=~", choice($.regex_literal, $.expression)), + // verify whether this is valid + $.expression, + ), + + // Capture this special for injections + regex_literal: ($) => prec(1, $.string), + + // value : NAME '(' sequence? ')' + // | BACKTICK + // | INDENTED_BACKTICK + // | NAME + // | string + // | '(' expression ')' + value: ($) => + prec.left( + choice( + $.function_call, + $.external_command, + $.identifier, + $.string, + $.numeric_error, + seq("(", $.expression, ")"), + ), + ), + + function_call: ($) => + seq( + field("name", $.identifier), + "(", + optional(field("arguments", $.sequence)), + ")", + ), + + external_command: ($) => + choice(seq($._backticked), seq($._indented_backticked)), + + // sequence : expression ',' sequence + // | expression ','? + sequence: ($) => comma_sep1($.expression), + + // Key=value argument for attributes like [arg("x", pattern='\d+')] + attribute_kv_argument: ($) => + seq(field("key", $.identifier), "=", field("value", $.string)), + + attribute: ($) => + seq( + "[", + comma_sep1( + choice( + $.identifier, + seq( + $.identifier, + "(", + field( + "argument", + comma_sep1( + choice($.string, $.identifier, $.attribute_kv_argument), + ), + ), + ")", + ), + seq($.identifier, ":", field("argument", $.string)), + ), + ), + "]", + $._newline, + ), + + // A complete recipe + // recipe : attribute? '@'? NAME parameter* variadic_parameters? ':' dependency* body? + recipe: ($) => + seq( + repeat($.attribute), + $.recipe_header, + $._newline, + optional($.recipe_body), + ), + + recipe_header: ($) => + seq( + optional("@"), + field("name", choice($.identifier, alias("import", $.identifier))), + optional($.parameters), + ":", + optional($.dependencies), + ), + + parameters: ($) => + seq(repeat($.parameter), choice($.parameter, $.variadic_parameter)), + + // FIXME: do we really have leading `$`s here?` + // parameter : '$'? NAME + // | '$'? NAME '=' value + parameter: ($) => + seq( + optional("$"), + field("name", $.identifier), + optional(seq("=", field("default", $.value))), + ), + + // variadic_parameters : '*' parameter + // | '+' parameter + variadic_parameter: ($) => + seq(field("kleene", choice("*", "+")), $.parameter), + + dependencies: ($) => repeat1(seq(optional("&&"), $.dependency)), + + // dependency : NAME + // | '(' NAME expression* ')' + dependency: ($) => + choice(field("name", $.identifier), $.dependency_expression), + + // contents of `(recipe expression)` + dependency_expression: ($) => + seq("(", field("name", $.identifier), repeat($.expression), ")"), + + // body : INDENT line+ DEDENT + recipe_body: ($) => + seq( + $._indent, + optional(seq(field("shebang", $.shebang), $._newline)), + repeat(choice(seq($.recipe_line, $._newline), $._newline)), + $._dedent, + ), + + recipe_line: ($) => + seq( + optional($.recipe_line_prefix), + repeat1(choice($.text, $.interpolation)), + ), + + recipe_line_prefix: (_) => choice("@-", "-@", "@", "-"), + + // Any shebang. Needs a named field to apply injection queries correctly. + shebang: ($) => + seq(/#![ \t]*/, choice($._shebang_with_lang, $._opaque_shebang)), + + // Shebang with a nested `language` token that we can extract + _shebang_with_lang: ($) => + seq( + /\S*\//, + optional(seq("env", repeat(SHEBANG_ENV_FLAG))), + alias($.identifier, $.language), + /.*/, + ), + + // Fallback shebang, any string + _opaque_shebang: (_) => /[^/\n]+/, + + // string : STRING + // | INDENTED_STRING + // | RAW_STRING + // | INDENTED_RAW_STRING + string: ($) => + choice( + $._string_indented, + $._raw_string_indented, + $._string, + // _raw_string, can't be written as a separate inline for osm reason + /'[^']*'/, + ), + + _raw_string_indented: (_) => seq("'''", repeat(/./), "'''"), + _string: ($) => seq('"', repeat(choice($.escape_sequence, /[^\\"]+/)), '"'), + // We need try two separate munches so neither escape sequences nor + // potential closing quotes get eaten. + _string_indented: ($) => + seq('"""', repeat(choice($.escape_sequence, /[^\\]?[^\\"]+/)), '"""'), + + escape_sequence: (_) => ESCAPE_SEQUENCE, + + _backticked: ($) => seq("`", optional($.command_body), "`"), + _indented_backticked: ($) => seq("```", optional($.command_body), "```"), + + command_body: ($) => repeat1(choice($.interpolation, /./)), + + // interpolation : '{{' expression '}}' + interpolation: ($) => seq("{{", $.expression, "}}"), + + identifier: (_) => /[a-zA-Z_][a-zA-Z0-9_-]*/, + + // Numbers aren't allowed as values, but we capture them anyway as errors so + // they don't mess up the whole syntax + numeric_error: (_) => /(\d+\.\d*|\d+)/, + + // `# ...` comment + comment: (_) => token(prec(-1, /#.*/)), + }, +}); diff --git a/langs/group-maple/just/def/grammar/scanner.c b/langs/group-maple/just/def/grammar/scanner.c new file mode 100644 index 00000000..2f05b37e --- /dev/null +++ b/langs/group-maple/just/def/grammar/scanner.c @@ -0,0 +1,317 @@ +#include "tree_sitter/parser.h" + +#include +#include +#include +#include + +#ifdef NDEBUG +#error "expected assertions to be enabled" +#endif + +// Enable this for debugging +// #define DEBUG_PRINT + +#ifndef __FILE_NAME__ +#define __FILE_NAME__ __FILE__ +#endif + +#ifdef __GNUC__ +#define unused_attr __attribute__((unused)) +#else +#define unused_attr +#endif + +#ifdef __wasm__ +#define assertf(...) (void)0; +#else + +#ifndef fprintf_s +#define fprintf_s fprintf // NOLINT +#endif + +#ifdef DEBUG_PRINT +#define dbg_print(...) \ + do { \ + fprintf_s(stderr, " \033[96;1mparse: \033[0m"); \ + fprintf_s(stderr, __VA_ARGS__); \ + } while (0) +#else +#define dbg_print(...) +#endif + +#define panic(...) \ + do { \ + fprintf_s(stderr, "panic at %s:%d: ", __FILE_NAME__, __LINE__); \ + fprintf_s(stderr, __VA_ARGS__); \ + fprintf_s(stderr, "\n"); \ + exit(1); \ + } while (0); + +#define assertf(condition, ...) \ + do { \ + if (__builtin_expect(!(condition), 0)) { \ + panic(__VA_ARGS__); \ + } \ + } while (0); + +#ifndef __GNUC__ +#define __builtin_expect(a, b) a +#endif + +#endif + +#define SBYTES sizeof(Scanner) + +enum TokenType { + INDENT, + DEDENT, + NEWLINE, + TEXT, + ERROR_RECOVERY, + TOKEN_TYPE_END, +}; + +unused_attr static inline void assert_valid_token(const TSSymbol sym) { + assertf(sym >= INDENT && sym < TOKEN_TYPE_END, "invalid symbol %d", sym); +} + +typedef struct Scanner { + uint32_t prev_indent; + uint16_t advance_brace_count; + bool has_seen_eof; +} Scanner; + +// This function should create your scanner object. It will only be called once +// anytime your language is set on a parser. Often, you will want to allocate +// memory on the heap and return a pointer to it. If your external scanner +// doesn’t need to maintain any state, it’s ok to return NULL. +void *tree_sitter_just_external_scanner_create(void) { + Scanner *ptr = (Scanner *)calloc(SBYTES, 1); + assertf(ptr, "tree_sitter_just_external_scanner_create: out of memory"); + return ptr; +} + +// This function should free any memory used by your scanner. It is called once +// when a parser is deleted or assigned a different language. It receives as an +// argument the same pointer that was returned from the create function. If your +// create function didn’t allocate any memory, this function can be a noop. +void tree_sitter_just_external_scanner_destroy(void *payload) { + assertf(payload, "got null payload at destroy"); + free(payload); +} + +// Serialize the state of the scanner. This is called when the parser is +// serialized. It receives as an argument the same pointer that was returned +// from the create function. +unsigned tree_sitter_just_external_scanner_serialize(void *payload, + char *buffer) { + assertf(SBYTES < TREE_SITTER_SERIALIZATION_BUFFER_SIZE, + "invalid scanner size"); + memcpy(buffer, payload, SBYTES); + return SBYTES; +} + +// Reconstruct a scanner from the serialized state. This is called when the +// parser is deserialized. +void tree_sitter_just_external_scanner_deserialize(void *payload, + const char *buffer, + unsigned length) { + Scanner *ptr = (Scanner *)payload; + if (length == 0) { + ptr->prev_indent = 0; + ptr->has_seen_eof = false; + return; + } + memcpy(ptr, buffer, SBYTES); +} + +// Continue and include the preceding character in the token +static inline void advance(TSLexer *lexer) { lexer->advance(lexer, false); } + +// Continue and discard the preceding character +static inline void skip(TSLexer *lexer) { lexer->advance(lexer, true); } + +// An EOF works as a dedent +static bool handle_eof(TSLexer *lexer, Scanner *state, + const bool *valid_symbols) { + assertf(lexer->eof(lexer), "expected EOF"); + lexer->mark_end(lexer); + + if (valid_symbols[DEDENT]) { + lexer->result_symbol = DEDENT; + return true; + } + + if (valid_symbols[NEWLINE]) { + if (state->has_seen_eof) { + // allow EOF to count for a single symbol. Don't return true more than + // once, otherwise it will keep calling us thinking there are more tokens. + return false; + } + + lexer->result_symbol = NEWLINE; + state->has_seen_eof = true; + return true; + } + return false; +} + +// This function is responsible for recognizing external tokens. It should +// return true if a token was recognized, and false otherwise. +bool tree_sitter_just_external_scanner_scan(void *payload, TSLexer *lexer, + const bool *valid_symbols) { + Scanner *scanner = (Scanner *)(payload); + + if (lexer->eof(lexer)) { + return handle_eof(lexer, scanner, valid_symbols); + } + + // Handle backslash escaping for newlines + if (valid_symbols[NEWLINE]) { + bool escape = false; + if (lexer->lookahead == '\\') { + escape = true; + skip(lexer); + } + + bool eol_found = false; + while (iswspace(lexer->lookahead)) { + if (lexer->lookahead == '\n') { + skip(lexer); + eol_found = true; + break; + } + skip(lexer); + } + + if (eol_found && !escape) { + lexer->result_symbol = NEWLINE; + return true; + } + } + + if (valid_symbols[INDENT] || valid_symbols[DEDENT]) { + while (!lexer->eof(lexer) && isspace(lexer->lookahead)) { + switch (lexer->lookahead) { + case '\n': + if (valid_symbols[INDENT]) { + return false; + } + + case '\t': + case ' ': + skip(lexer); + break; + + default: + return false; + } + } + + if (lexer->eof(lexer)) { + return handle_eof(lexer, scanner, valid_symbols); + } + + uint32_t indent = lexer->get_column(lexer); + + if (indent > scanner->prev_indent && valid_symbols[INDENT] && + scanner->prev_indent == 0) { + lexer->result_symbol = INDENT; + scanner->prev_indent = indent; + return true; + } + if (indent < scanner->prev_indent && valid_symbols[DEDENT] && indent == 0) { + lexer->result_symbol = DEDENT; + scanner->prev_indent = indent; + return true; + } + } + + if (valid_symbols[TEXT]) { + if (lexer->get_column(lexer) == scanner->prev_indent && + (lexer->lookahead == '\n' || lexer->lookahead == '@' || + lexer->lookahead == '-')) { + return false; + } + + bool advanced_once = false; + + while (lexer->lookahead == '{' && scanner->advance_brace_count > 0 && + !lexer->eof(lexer)) { + scanner->advance_brace_count--; + advance(lexer); + advanced_once = true; + } + + while (1) { + if (lexer->eof(lexer)) { + return handle_eof(lexer, scanner, valid_symbols); + } + + while (!lexer->eof(lexer) && lexer->lookahead != '\n' && + lexer->lookahead != '{') { + // Can't start with #! + if (lexer->lookahead == '#' && !advanced_once) { + advance(lexer); + if (lexer->lookahead == '!') { + return false; + } + } + + advance(lexer); + advanced_once = true; + } + + if (lexer->lookahead == '\n' || lexer->eof(lexer)) { + lexer->mark_end(lexer); + lexer->result_symbol = TEXT; + if (advanced_once) { + return true; + } + if (lexer->eof(lexer)) { + return handle_eof(lexer, scanner, valid_symbols); + } + advance(lexer); + } else if (lexer->lookahead == '{') { + lexer->mark_end(lexer); + advance(lexer); + + if (lexer->eof(lexer) || + lexer->lookahead == '\n') { // EOF without anything after { + lexer->mark_end(lexer); + lexer->result_symbol = TEXT; + return advanced_once; + } + + if (lexer->lookahead == '{') { + advance(lexer); + + while (lexer->lookahead == '{') { // more braces! + scanner->advance_brace_count++; + advance(lexer); + } + + // scan till a balanced pair of }} are found, then assume it's a valid + // interpolation + while (!lexer->eof(lexer) && lexer->lookahead != '\n') { + advance(lexer); + if (lexer->lookahead == '}') { + advance(lexer); + if (lexer->lookahead == '}') { + lexer->result_symbol = TEXT; + return advanced_once; + } + } + } + + if (!advanced_once) { + return false; + } + } + } + } + } + + return false; +} diff --git a/langs/group-maple/just/def/queries/folds.scm b/langs/group-maple/just/def/queries/folds.scm new file mode 100644 index 00000000..2640f4c4 --- /dev/null +++ b/langs/group-maple/just/def/queries/folds.scm @@ -0,0 +1,8 @@ +; Define collapse points + +([ + (recipe) + (string) + (external_command) +] @fold + (#trim! @fold)) diff --git a/langs/group-maple/just/def/queries/highlights.scm b/langs/group-maple/just/def/queries/highlights.scm new file mode 100644 index 00000000..e4dc5635 --- /dev/null +++ b/langs/group-maple/just/def/queries/highlights.scm @@ -0,0 +1,156 @@ +; This file specifies how matched syntax patterns should be highlighted + +[ + "export" + "import" +] @keyword.control.import + +"mod" @keyword.module + +[ + "alias" + "set" + "shell" +] @keyword + +[ + "if" + "else" +] @keyword.control.conditional + +; Variables + +(value + (identifier) @variable) + +(alias + left: (identifier) @variable) + +(assignment + left: (identifier) @variable) + +; Functions + +(recipe_header + name: (identifier) @function) + +(dependency + name: (identifier) @function.call) + +(dependency_expression + name: (identifier) @function.call) + +(function_call + name: (identifier) @function.call) + +; Parameters + +(parameter + name: (identifier) @variable.parameter) + +; Namespaces + +(module + name: (identifier) @namespace) + +; Operators + +[ + ":=" + "?" + "==" + "!=" + "=~" + "@" + "=" + "$" + "*" + "+" + "&&" + "@-" + "-@" + "-" + "/" + ":" +] @operator + +; Punctuation + +"," @punctuation.delimiter + +[ + "{" + "}" + "[" + "]" + "(" + ")" + "{{" + "}}" +] @punctuation.bracket + +[ "`" "```" ] @punctuation.special + +; Literals + +(boolean) @constant.builtin.boolean + +[ + (string) + (external_command) +] @string + +(escape_sequence) @constant.character.escape + +; Comments + +(comment) @spell @comment.line + +(shebang) @keyword.directive + +; highlight known settings (filtering does not always work) +(setting + left: (identifier) @keyword + (#any-of? @keyword + "allow-duplicate-recipes" + "allow-duplicate-variables" + "dotenv-filename" + "dotenv-load" + "dotenv-path" + "dotenv-required" + "export" + "fallback" + "ignore-comments" + "positional-arguments" + "shell" + "shell-interpreter" + "tempdir" + "windows-powershell" + "windows-shell" + "working-directory")) + +; highlight known attributes (filtering does not always work) +(attribute + (identifier) @attribute + (#any-of? @attribute + "confirm" + "doc" + "extension" + "group" + "linux" + "macos" + "metadata" + "no-cd" + "no-exit-message" + "no-quiet" + "openbsd" + "parallel" + "positional-arguments" + "private" + "script" + "unix" + "windows" + "working-directory")) + +; Numbers are part of the syntax tree, even if disallowed +(numeric_error) @error diff --git a/langs/group-maple/just/def/queries/indents.scm b/langs/group-maple/just/def/queries/indents.scm new file mode 100644 index 00000000..f292688a --- /dev/null +++ b/langs/group-maple/just/def/queries/indents.scm @@ -0,0 +1,11 @@ +; This query specifies how to auto-indent logical blocks. +; +; Better documentation with diagrams is in https://docs.helix-editor.com/guides/indent.html SKIP-NVIM SKIP-NVIM-NEXT + +[ + (recipe) + (string) + (external_command) +] @indent @extend + +(comment) @indent.auto ; SKIP-HELIX SKIP-ZED SKIP-LAPCE diff --git a/langs/group-maple/just/def/queries/injections.scm b/langs/group-maple/just/def/queries/injections.scm new file mode 100644 index 00000000..8ba4c05e --- /dev/null +++ b/langs/group-maple/just/def/queries/injections.scm @@ -0,0 +1,111 @@ +; Specify nested languages that live within a `justfile` + +; FIXME: these are not compatible with helix due to precedence SKIP-NVIM SKIP-NVIM-OLD + +; ================ Always applicable ================ + +((comment) @injection.content + (#set! injection.language "comment")) + +; Highlight the RHS of `=~` as regex +((regex_literal + (_) @injection.content) + (#set! injection.language "regex")) + +; ================ Global defaults ================ + +; Default everything to be bash +(recipe_body + !shebang + (#set! injection.language "bash") + (#set! injection.include-children)) @injection.content + +(external_command + (command_body) @injection.content + (#set! injection.language "bash")) + +; ================ Global language specified ================ +; Global language is set with something like one of the following: +; +; set shell := ["bash", "-c", ...] +; set shell := ["pwsh.exe"] +; +; We can extract the first item of the array, but we can't extract the language +; name from the string with something like regex. So instead we special case +; two things: powershell, which is likely to come with a `.exe` attachment that +; we need to strip, and everything else which hopefully has no extension. We +; separate this with a `#match?`. +; +; Unfortunately, there also isn't a way to allow arbitrary nesting or +; alternatively set "global" capture variables. So we can set this for item- +; level external commands, but not for e.g. external commands within an +; expression without getting _really_ annoying. Should at least look fine since +; they default to bash. Limitations... +; See https://github.com/tree-sitter/tree-sitter/issues/880 for more on that. + +(source_file + (setting "shell" ":=" "[" (string) @_langstr + (#match? @_langstr ".*(powershell|pwsh|cmd).*") + (#set! injection.language "powershell")) + [ + (recipe + (recipe_body + !shebang + (#set! injection.include-children)) @injection.content) + + (assignment + (expression + (value + (external_command + (command_body) @injection.content)))) + ]) + +(source_file + (setting "shell" ":=" "[" (string) @injection.language + (#not-match? @injection.language ".*(powershell|pwsh|cmd).*")) + [ + (recipe + (recipe_body + !shebang + (#set! injection.include-children)) @injection.content) + + (assignment + (expression + (value + (external_command + (command_body) @injection.content)))) + ]) + +; ================ Recipe language specified ================ ; SKIP-HELIX + ; SKIP-HELIX +; Set highlighting for recipes that specify a language, using the exact name by default ; SKIP-HELIX +(recipe_body ; ; SKIP-HELIX + (shebang ; ; SKIP-HELIX + (language) @injection.language) ; SKIP-HELIX + (#not-any-of? @injection.language "python3" "nodejs" "node" "uv") ; SKIP-HELIX + (#set! injection.include-children)) @injection.content ; SKIP-HELIX + ; SKIP-HELIX +; Transform some known executables ; SKIP-HELIX + ; SKIP-HELIX +; python3/uv -> python ; SKIP-HELIX +(recipe_body ; SKIP-HELIX + (shebang ; SKIP-HELIX + (language) @_lang) ; SKIP-HELIX + (#any-of? @_lang "python3" "uv") ; SKIP-HELIX + (#set! injection.language "python") ; SKIP-HELIX + (#set! injection.include-children)) @injection.content ; SKIP-HELIX + ; SKIP-HELIX +; node/nodejs -> javascript ; SKIP-HELIX +(recipe_body ; SKIP-HELIX + (shebang ; SKIP-HELIX + (language) @_lang) ; SKIP-HELIX + (#any-of? @_lang "node" "nodejs") ; SKIP-HELIX + (#set! injection.language "javascript") ; SKIP-HELIX + (#set! injection.include-children)) @injection.content ; SKIP-HELIX + +; ================ Recipe language specified - Helix only ================ ; SKIP-NVIM SKIP-NVIM-OLD SKIP-LAPCE SKIP-ZED + ; SKIP-NVIM SKIP-NVIM-OLD SKIP-LAPCE SKIP-ZED +; Set highlighting for recipes that specify a language using builtin shebang matching ; SKIP-NVIM SKIP-NVIM-OLD SKIP-LAPCE SKIP-ZED +(recipe_body ; SKIP-NVIM SKIP-NVIM-OLD SKIP-LAPCE SKIP-ZED + (shebang) @injection.shebang ; SKIP-NVIM SKIP-NVIM-OLD SKIP-LAPCE SKIP-ZED + (#set! injection.include-children)) @injection.content ; SKIP-NVIM SKIP-NVIM-OLD SKIP-LAPCE SKIP-ZED diff --git a/langs/group-maple/just/def/queries/locals.scm b/langs/group-maple/just/def/queries/locals.scm new file mode 100644 index 00000000..64351f79 --- /dev/null +++ b/langs/group-maple/just/def/queries/locals.scm @@ -0,0 +1,40 @@ +; This file tells us about the scope of variables so e.g. local +; variables override global functions with the same name + +; Scope + +(recipe) @local.scope + +; Definitions + +(alias + left: (identifier) @local.definition.variable) + +(assignment + left: (identifier) @local.definition.variable) + +(module + name: (identifier) @local.definition.namespace) + +(parameter + name: (identifier) @local.definition.variable) + +(recipe_header + name: (identifier) @local.definition.function) + +; References + +(alias + right: (identifier) @local.reference.variable) + +(function_call + name: (identifier) @local.reference.function) + +(dependency + name: (identifier) @local.reference.function) + +(dependency_expression + name: (identifier) @local.reference.function) + +(value + (identifier) @local.reference.variable) diff --git a/langs/group-maple/just/def/queries/textobjects.scm b/langs/group-maple/just/def/queries/textobjects.scm new file mode 100644 index 00000000..3a2633cc --- /dev/null +++ b/langs/group-maple/just/def/queries/textobjects.scm @@ -0,0 +1,16 @@ +; Specify how to navigate around logical blocks in code + +(recipe + (recipe_body) @function.inside) @function.around + +(parameters + ((_) @parameter.inside . ","? @parameter.around)) @parameter.around + +(dependency_expression + (_) @parameter.inside) @parameter.around + +(function_call + arguments: (sequence + (expression) @parameter.inside) @parameter.around) @function.around + +(comment) @comment.around diff --git a/langs/group-maple/just/def/samples/Justfile b/langs/group-maple/just/def/samples/Justfile new file mode 100644 index 00000000..bf55f276 --- /dev/null +++ b/langs/group-maple/just/def/samples/Justfile @@ -0,0 +1,26 @@ +# Justfile for arborium development tasks + +# Docker image settings +docker_image := "ghcr.io/bearcove/arborium-plugin-builder" +docker_tag := "latest" + +# Build the plugin-builder Docker image (for linux/amd64 - GitHub Actions runners) +docker-build: + docker build --platform linux/amd64 -t {{docker_image}}:{{docker_tag}} -f Dockerfile.ci . + +# Push the plugin-builder Docker image to GHCR +docker-push: docker-build + docker push {{docker_image}}:{{docker_tag}} + +# Build and push with a specific tag +docker-release tag: + docker build --platform linux/amd64 -t {{docker_image}}:{{tag}} -f Dockerfile.ci . + docker push {{docker_image}}:{{tag}} + +# Login to GitHub Container Registry (requires GITHUB_TOKEN env var or gh auth) +docker-login: + @echo "Logging in to ghcr.io..." + @gh auth token | docker login ghcr.io -u $(gh api user --jq .login) --password-stdin + +grammar-test lang: + cargo xtask grammar-test {{lang}}