├── .clang-tidy ├── .editorconfig ├── .envrc ├── .git-blame-ignore-revs ├── .gitattributes ├── .github └── workflows │ ├── ci.yaml │ └── fuzz.yaml ├── .gitignore ├── .npmignore ├── .prettierignore ├── Cargo.lock ├── Cargo.toml ├── GRAMMAR.md ├── LICENSE ├── Makefile ├── Package.swift ├── README.md ├── binding.gyp ├── bindings ├── c │ ├── tree-sitter-just.h │ └── tree-sitter-just.pc.in ├── debug.c ├── go │ ├── binding.go │ ├── binding_test.go │ └── go.mod ├── node │ ├── binding.cc │ ├── index.d.ts │ └── index.js ├── python │ └── tree_sitter_just │ │ ├── __init__.py │ │ ├── __init__.pyi │ │ ├── binding.c │ │ └── py.typed ├── rust │ ├── build.rs │ └── lib.rs └── swift │ └── TreeSitterJust │ └── just.h ├── build-flavored-queries.py ├── eslint.config.mjs ├── flake.lock ├── flake.nix ├── ftdetect └── just.vim ├── fuzzer ├── build-corpus.py └── entry.c ├── grammar.js ├── justfile ├── lua └── tree-sitter-just │ └── init.lua ├── package-lock.json ├── package.json ├── pyproject.toml ├── queries-flavored ├── helix │ ├── folds.scm │ ├── highlights.scm │ ├── indents.scm │ ├── injections.scm │ ├── locals.scm │ └── textobjects.scm ├── lapce │ ├── folds.scm │ ├── highlights.scm │ ├── indents.scm │ ├── injections.scm │ ├── locals.scm │ └── textobjects.scm └── zed │ ├── folds.scm │ ├── highlights.scm │ ├── indents.scm │ ├── injections.scm │ ├── locals.scm │ └── textobjects.scm ├── queries-src ├── folds.scm ├── highlights.scm ├── indents.scm ├── injections.scm ├── locals.scm └── textobjects.scm ├── queries └── just │ ├── folds.scm │ ├── highlights.scm │ ├── indents.scm │ ├── injections.scm │ ├── locals.scm │ └── textobjects.scm ├── src ├── grammar.json ├── node-types.json ├── parser.c ├── scanner.c └── tree_sitter │ ├── alloc.h │ ├── array.h │ └── parser.h ├── test ├── corpus │ ├── delimited.txt │ ├── injections.txt │ ├── multiline.txt │ ├── recipes.txt │ └── statements.txt ├── crash-4b0422bb457cd6b39d1f8549f6739830254718a0z-assertion ├── highlight │ ├── injections-global-pwsh.just │ ├── injections-global-py.just │ ├── injections.just │ ├── invalid-syntax.just │ ├── multiline.just │ ├── recipes.just │ └── statements.just ├── issue69-segfault.just ├── readme.just ├── test.just └── timeout-1aa6bf37e914715f4aa49e6cf693f7abf81aaf8e └── tree-sitter.json /.clang-tidy: -------------------------------------------------------------------------------- 1 | Checks: "clang-diagnostic-*,clang-analyzer-*,cppcoreguidelines-*,modernize-*,-modernize-use-trailing-return-type" 2 | WarningsAsErrors: true 3 | HeaderFilterRegex: "" 4 | FormatStyle: google 5 | ExtraArgsBefore: ["--std=c11"] 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | 9 | [*.{json,toml,yml,gyp}] 10 | indent_style = space 11 | indent_size = 2 12 | 13 | [*.js] 14 | indent_style = space 15 | indent_size = 2 16 | 17 | [*.rs] 18 | indent_style = space 19 | indent_size = 4 20 | 21 | [*.{c,cc,h}] 22 | indent_style = space 23 | indent_size = 4 24 | 25 | [*.{py,pyi}] 26 | indent_style = space 27 | indent_size = 4 28 | 29 | [*.swift] 30 | indent_style = space 31 | indent_size = 4 32 | 33 | [*.go] 34 | indent_style = tab 35 | indent_size = 8 36 | 37 | [Makefile] 38 | indent_style = tab 39 | indent_size = 8 40 | 41 | [justfile] 42 | indent_style = tab 43 | indent_size = 4 44 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use_flake 2 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Reformatting with prettier 2 | 2cf6e8a21f247adcfd1b54e0043183057880cdee 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /src/** linguist-vendored 2 | /examples/* linguist-vendored 3 | 4 | src/grammar.json linguist-generated 5 | src/node-types.json linguist-generated 6 | src/parser.c linguist-generated 7 | src/tree_sitter/alloc.h linguist-generated 8 | src/tree_sitter/array.h linguist-generated 9 | src/tree_sitter/parser.h linguist-generated 10 | 11 | src/grammar.json -diff 12 | src/node-types.json -diff 13 | src/parser.c -diff 14 | src/tree_sitter/alloc.h -diff 15 | src/tree_sitter/array.h -diff 16 | src/tree_sitter/parser.h -diff 17 | 18 | # These test files are from the fuzzer 19 | test/**timeout-* binary 20 | test/**crash-* binary 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | pull_request: 4 | push: 5 | branches: [main] 6 | 7 | env: 8 | JUST_VERBOSE: 1 9 | RUST_BACKTRACE: 1 10 | CI: 1 11 | 12 | jobs: 13 | codestyle: 14 | name: codestyle & generated files 15 | runs-on: ubuntu-latest 16 | timeout-minutes: 15 17 | steps: 18 | - uses: actions/checkout@v4 19 | - uses: taiki-e/install-action@just 20 | - uses: actions/setup-node@v4 21 | with: 22 | node-version: 20 23 | - run: pip install ruff 24 | - name: Get npm cache directory 25 | id: npm-cache-dir 26 | shell: bash 27 | run: echo "dir=$(npm config get cache)" >> ${GITHUB_OUTPUT} 28 | - uses: actions/cache@v4 29 | id: npm-cache 30 | with: 31 | path: ${{ steps.npm-cache-dir.outputs.dir }} 32 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 33 | restore-keys: ${{ runner.os }}-node- 34 | - run: just setup 35 | - name: Verify generated files are up to date (error) 36 | run: just ci-validate-generated-files 37 | - name: Check codestyle 38 | run: just ci-codestyle 39 | 40 | test: 41 | runs-on: ${{ matrix.os }} 42 | timeout-minutes: 15 43 | strategy: 44 | fail-fast: true 45 | matrix: 46 | os: [macos-latest, ubuntu-latest, windows-latest] 47 | steps: 48 | - uses: actions/checkout@v4 49 | - uses: taiki-e/install-action@just 50 | - uses: actions/setup-node@v4 51 | - uses: mymindstorm/setup-emsdk@v14 52 | with: 53 | node-version: 20 54 | - name: Get npm cache directory 55 | id: npm-cache-dir 56 | shell: bash 57 | run: echo "dir=$(npm config get cache)" >> ${GITHUB_OUTPUT} 58 | - uses: actions/cache@v4 59 | id: npm-cache 60 | with: 61 | path: ${{ steps.npm-cache-dir.outputs.dir }} 62 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 63 | restore-keys: ${{ runner.os }}-node- 64 | - run: just setup --locked 65 | - name: Configure 66 | run: just configure-tree-sitter 67 | - name: Run tests 68 | run: just test 69 | - name: Check if generated files are up to date (warn only) 70 | run: just ci-validate-generated-files 0 71 | - name: Test WASM build 72 | run: just build-wasm 73 | 74 | success: 75 | needs: 76 | - codestyle 77 | - test 78 | runs-on: ubuntu-latest 79 | # GitHub branch protection is exceedingly silly and treats "jobs skipped because a dependency 80 | # failed" as success. So we have to do some contortions to ensure the job fails if any of its 81 | # dependencies fails. 82 | if: always() # make sure this is never "skipped" 83 | steps: 84 | # Manually check the status of all dependencies. `if: failure()` does not work. 85 | - name: check if any dependency failed 86 | run: jq --exit-status 'all(.result == "success")' <<< '${{ toJson(needs) }}' 87 | -------------------------------------------------------------------------------- /.github/workflows/fuzz.yaml: -------------------------------------------------------------------------------- 1 | name: Fuzz Parser 2 | 3 | on: 4 | push: 5 | paths: 6 | - src/scanner.c 7 | - .github/workflows/fuzz.yaml 8 | - fuzzer/ 9 | pull_request: 10 | paths: 11 | - src/scanner.c 12 | - .github/workflows/fuzz.yaml 13 | - fuzzer/ 14 | workflow_dispatch: 15 | 16 | env: 17 | RUST_BACKTRACE: 1 18 | CI: 1 19 | 20 | jobs: 21 | test: 22 | name: Parser fuzzing 23 | runs-on: ubuntu-latest 24 | timeout-minutes: 25 25 | # Run in a container because we had some issues reproducing failures 26 | container: 27 | image: node:lts-slim 28 | env: 29 | # Just 10 minutes as a sanity check, should run longer locally. 30 | # For whatever reason, this time is always wayyyy exceeded 31 | # (hence the longer workflow timeout) 32 | FUZZ_TOTAL_TIME: 600 33 | steps: 34 | - uses: actions/checkout@v4 35 | - run: | 36 | if [ -f /.dockerenv ]; then 37 | echo "Running in docker" 38 | else 39 | echo "Not in a docker container!" 40 | exit 1 41 | fi 42 | 43 | apt-get update 44 | apt-get install -y clang curl make g++ git 45 | curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | 46 | bash -s -- --to /usr/bin 47 | - run: just setup 48 | - run: just fuzz 49 | - name: Print failures 50 | if: always() 51 | run: ls fuzzer/failures/* | 52 | xargs -IFNAME sh -c 'echo "\nContents of FNAME:" && base64 -i FNAME' 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Rust build 2 | target/** 3 | 4 | # C/C++ language server cache 5 | .ccls-cache/** 6 | 7 | # Node cache 8 | node_modules 9 | 10 | # Tree-sitter ouptut 11 | build/ 12 | .build/ 13 | log.html 14 | *.wasm 15 | *.dylib 16 | fuzzer-out/ 17 | 18 | # Fuzzer items 19 | tree-sitter-src/ 20 | fuzzer/failures/ 21 | fuzzer/corpus 22 | fuzzer/artifacts 23 | **.log 24 | **.out 25 | **.dSYM 26 | 27 | # Windows things 28 | *.exp 29 | *.lib 30 | *.obj 31 | 32 | # Other repos 33 | repositories/ 34 | 35 | # Editor files 36 | compile_commands.json 37 | .vscode/ 38 | *.swp 39 | 40 | # No need to support ancient python 41 | setup.py 42 | 43 | # Configuring includes for autocompletion in C files 44 | .clangd 45 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /test 2 | /examples 3 | /build 4 | /script 5 | /target 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ 3 | repositories/ 4 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "cc" 16 | version = "1.2.1" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" 19 | dependencies = [ 20 | "shlex", 21 | ] 22 | 23 | [[package]] 24 | name = "memchr" 25 | version = "2.7.4" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 28 | 29 | [[package]] 30 | name = "regex" 31 | version = "1.10.6" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" 34 | dependencies = [ 35 | "aho-corasick", 36 | "memchr", 37 | "regex-automata", 38 | "regex-syntax", 39 | ] 40 | 41 | [[package]] 42 | name = "regex-automata" 43 | version = "0.4.7" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" 46 | dependencies = [ 47 | "aho-corasick", 48 | "memchr", 49 | "regex-syntax", 50 | ] 51 | 52 | [[package]] 53 | name = "regex-syntax" 54 | version = "0.8.4" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" 57 | 58 | [[package]] 59 | name = "shlex" 60 | version = "1.3.0" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 63 | 64 | [[package]] 65 | name = "streaming-iterator" 66 | version = "0.1.9" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" 69 | 70 | [[package]] 71 | name = "tree-sitter" 72 | version = "0.24.4" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "b67baf55e7e1b6806063b1e51041069c90afff16afcbbccd278d899f9d84bca4" 75 | dependencies = [ 76 | "cc", 77 | "regex", 78 | "regex-syntax", 79 | "streaming-iterator", 80 | "tree-sitter-language", 81 | ] 82 | 83 | [[package]] 84 | name = "tree-sitter-just" 85 | version = "0.1.0" 86 | dependencies = [ 87 | "cc", 88 | "tree-sitter", 89 | ] 90 | 91 | [[package]] 92 | name = "tree-sitter-language" 93 | version = "0.1.2" 94 | source = "registry+https://github.com/rust-lang/crates.io-index" 95 | checksum = "e8ddffe35a0e5eeeadf13ff7350af564c6e73993a24db62caee1822b185c2600" 96 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tree-sitter-just" 3 | description = "just grammar for the tree-sitter parsing library" 4 | version = "0.1.0" 5 | keywords = ["incremental", "parsing", "just"] 6 | categories = ["parsing", "text-editors"] 7 | repository = "https://github.com/IndianBoy42/tree-sitter-just" 8 | edition = "2021" 9 | license = "MIT" 10 | autoexamples = false 11 | 12 | build = "bindings/rust/build.rs" 13 | include = ["bindings/rust/*", "grammar.js", "queries-flavored/helix/*", "src/*"] 14 | 15 | [lib] 16 | path = "bindings/rust/lib.rs" 17 | 18 | [dependencies] 19 | tree-sitter = "~0.24.4" 20 | 21 | [build-dependencies] 22 | cc = "1.2.1" 23 | -------------------------------------------------------------------------------- /GRAMMAR.md: -------------------------------------------------------------------------------- 1 | # justfile grammar 2 | 3 | Justfiles are processed by a mildly context-sensitive tokenizer 4 | and a recursive descent parser. The grammar is LL(k), for an 5 | unknown but hopefully reasonable value of k. 6 | 7 | ## tokens 8 | 9 | ```` 10 | BACKTICK = `[^`]*` 11 | INDENTED_BACKTICK = ```[^(```)]*``` 12 | COMMENT = #([^!].*)?$ 13 | DEDENT = emitted when indentation decreases 14 | EOF = emitted at the end of the file 15 | INDENT = emitted when indentation increases 16 | LINE = emitted before a recipe line 17 | NAME = [a-zA-Z_][a-zA-Z0-9_-]* 18 | NEWLINE = \n|\r\n 19 | RAW_STRING = '[^']*' 20 | INDENTED_RAW_STRING = '''[^(''')]*''' 21 | STRING = "[^"]*" # also processes \n \r \t \" \\ escapes 22 | INDENTED_STRING = """[^("""]*""" # also processes \n \r \t \" \\ escapes 23 | TEXT = recipe text, only matches in a recipe body 24 | ```` 25 | 26 | ## grammar syntax 27 | 28 | ``` 29 | | alternation 30 | () grouping 31 | _? option (0 or 1 times) 32 | _* repetition (0 or more times) 33 | _+ repetition (1 or more times) 34 | ``` 35 | 36 | ## grammar 37 | 38 | ``` 39 | justfile : item* EOF 40 | 41 | item : recipe 42 | | alias 43 | | assignment 44 | | export 45 | | import 46 | | module 47 | | setting 48 | | eol 49 | 50 | eol : NEWLINE 51 | | COMMENT NEWLINE 52 | 53 | alias : 'alias' NAME ':=' NAME 54 | 55 | assignment : NAME ':=' expression eol 56 | 57 | export : 'export' assignment 58 | 59 | setting : 'set' 'dotenv-load' boolean? 60 | | 'set' 'export' boolean? 61 | | 'set' 'positional-arguments' boolean? 62 | | 'set' 'shell' ':=' '[' string (',' string)* ','? ']' 63 | 64 | import : 'import' '?'? string? 65 | 66 | module : 'mod' '?'? NAME string? 67 | 68 | boolean : ':=' ('true' | 'false') 69 | 70 | expression : 'if' condition '{' expression '}' 'else' '{' expression '}' 71 | | value '/' expression 72 | | value '+' expression 73 | | value 74 | 75 | condition : expression '==' expression 76 | | expression '!=' expression 77 | | expression '=~' expression 78 | 79 | value : NAME '(' sequence? ')' 80 | | BACKTICK 81 | | INDENTED_BACKTICK 82 | | NAME 83 | | string 84 | | '(' expression ')' 85 | 86 | string : STRING 87 | | INDENTED_STRING 88 | | RAW_STRING 89 | | INDENTED_RAW_STRING 90 | 91 | sequence : expression ',' sequence 92 | | expression ','? 93 | 94 | recipe : attribute? '@'? NAME parameter* variadic? ':' dependency* body? 95 | 96 | attribute : '[' NAME ']' eol 97 | 98 | parameter : '$'? NAME 99 | | '$'? NAME '=' value 100 | 101 | variadic : '*' parameter 102 | | '+' parameter 103 | 104 | dependency : NAME 105 | | '(' NAME expression* ')' 106 | 107 | body : INDENT line+ DEDENT 108 | 109 | line : LINE (TEXT | interpolation)+ NEWLINE 110 | | NEWLINE 111 | 112 | interpolation : '{{' expression '}}' 113 | ``` 114 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION := 0.0.1 2 | 3 | LANGUAGE_NAME := tree-sitter-just 4 | 5 | # repository 6 | SRC_DIR := src 7 | 8 | PARSER_REPO_URL := $(shell git -C $(SRC_DIR) remote get-url origin 2>/dev/null) 9 | 10 | ifeq ($(PARSER_URL),) 11 | PARSER_URL := $(subst .git,,$(PARSER_REPO_URL)) 12 | ifeq ($(shell echo $(PARSER_URL) | grep '^[a-z][-+.0-9a-z]*://'),) 13 | PARSER_URL := $(subst :,/,$(PARSER_URL)) 14 | PARSER_URL := $(subst git@,https://,$(PARSER_URL)) 15 | endif 16 | endif 17 | 18 | TS ?= tree-sitter 19 | 20 | # ABI versioning 21 | SONAME_MAJOR := $(word 1,$(subst ., ,$(VERSION))) 22 | SONAME_MINOR := $(word 2,$(subst ., ,$(VERSION))) 23 | 24 | # install directory layout 25 | PREFIX ?= /usr/local 26 | INCLUDEDIR ?= $(PREFIX)/include 27 | LIBDIR ?= $(PREFIX)/lib 28 | PCLIBDIR ?= $(LIBDIR)/pkgconfig 29 | 30 | # source/object files 31 | PARSER := $(SRC_DIR)/parser.c 32 | EXTRAS := $(filter-out $(PARSER),$(wildcard $(SRC_DIR)/*.c)) 33 | OBJS := $(patsubst %.c,%.o,$(PARSER) $(EXTRAS)) 34 | 35 | # flags 36 | ARFLAGS ?= rcs 37 | override CFLAGS += -I$(SRC_DIR) -std=c11 -fPIC 38 | 39 | # OS-specific bits 40 | ifeq ($(OS),Windows_NT) 41 | $(error "Windows is not supported") 42 | else ifeq ($(shell uname),Darwin) 43 | SOEXT = dylib 44 | SOEXTVER_MAJOR = $(SONAME_MAJOR).dylib 45 | SOEXTVER = $(SONAME_MAJOR).$(SONAME_MINOR).dylib 46 | LINKSHARED := $(LINKSHARED)-dynamiclib -Wl, 47 | ifneq ($(ADDITIONAL_LIBS),) 48 | LINKSHARED := $(LINKSHARED)$(ADDITIONAL_LIBS), 49 | endif 50 | LINKSHARED := $(LINKSHARED)-install_name,$(LIBDIR)/lib$(LANGUAGE_NAME).$(SONAME_MAJOR).dylib,-rpath,@executable_path/../Frameworks 51 | else 52 | SOEXT = so 53 | SOEXTVER_MAJOR = so.$(SONAME_MAJOR) 54 | SOEXTVER = so.$(SONAME_MAJOR).$(SONAME_MINOR) 55 | LINKSHARED := $(LINKSHARED)-shared -Wl, 56 | ifneq ($(ADDITIONAL_LIBS),) 57 | LINKSHARED := $(LINKSHARED)$(ADDITIONAL_LIBS) 58 | endif 59 | LINKSHARED := $(LINKSHARED)-soname,lib$(LANGUAGE_NAME).so.$(SONAME_MAJOR) 60 | endif 61 | ifneq ($(filter $(shell uname),FreeBSD NetBSD DragonFly),) 62 | PCLIBDIR := $(PREFIX)/libdata/pkgconfig 63 | endif 64 | 65 | all: lib$(LANGUAGE_NAME).a lib$(LANGUAGE_NAME).$(SOEXT) $(LANGUAGE_NAME).pc 66 | 67 | lib$(LANGUAGE_NAME).a: $(OBJS) 68 | $(AR) $(ARFLAGS) $@ $^ 69 | 70 | lib$(LANGUAGE_NAME).$(SOEXT): $(OBJS) 71 | $(CC) $(LDFLAGS) $(LINKSHARED) $^ $(LDLIBS) -o $@ 72 | ifneq ($(STRIP),) 73 | $(STRIP) $@ 74 | endif 75 | 76 | $(LANGUAGE_NAME).pc: bindings/c/$(LANGUAGE_NAME).pc.in 77 | sed -e 's|@URL@|$(PARSER_URL)|' \ 78 | -e 's|@VERSION@|$(VERSION)|' \ 79 | -e 's|@LIBDIR@|$(LIBDIR)|' \ 80 | -e 's|@INCLUDEDIR@|$(INCLUDEDIR)|' \ 81 | -e 's|@REQUIRES@|$(REQUIRES)|' \ 82 | -e 's|@ADDITIONAL_LIBS@|$(ADDITIONAL_LIBS)|' \ 83 | -e 's|=$(PREFIX)|=$${prefix}|' \ 84 | -e 's|@PREFIX@|$(PREFIX)|' $< > $@ 85 | 86 | $(PARSER): $(SRC_DIR)/grammar.json 87 | $(TS) generate --no-bindings $^ 88 | 89 | install: all 90 | install -d '$(DESTDIR)$(INCLUDEDIR)'/tree_sitter '$(DESTDIR)$(PCLIBDIR)' '$(DESTDIR)$(LIBDIR)' 91 | install -m644 bindings/c/$(LANGUAGE_NAME).h '$(DESTDIR)$(INCLUDEDIR)'/tree_sitter/$(LANGUAGE_NAME).h 92 | install -m644 $(LANGUAGE_NAME).pc '$(DESTDIR)$(PCLIBDIR)'/$(LANGUAGE_NAME).pc 93 | install -m644 lib$(LANGUAGE_NAME).a '$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).a 94 | install -m755 lib$(LANGUAGE_NAME).$(SOEXT) '$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).$(SOEXTVER) 95 | ln -sf lib$(LANGUAGE_NAME).$(SOEXTVER) '$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).$(SOEXTVER_MAJOR) 96 | ln -sf lib$(LANGUAGE_NAME).$(SOEXTVER_MAJOR) '$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).$(SOEXT) 97 | 98 | uninstall: 99 | $(RM) '$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).a \ 100 | '$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).$(SOEXTVER) \ 101 | '$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).$(SOEXTVER_MAJOR) \ 102 | '$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).$(SOEXT) \ 103 | '$(DESTDIR)$(INCLUDEDIR)'/tree_sitter/$(LANGUAGE_NAME).h \ 104 | '$(DESTDIR)$(PCLIBDIR)'/$(LANGUAGE_NAME).pc 105 | 106 | clean: 107 | $(RM) $(OBJS) $(LANGUAGE_NAME).pc lib$(LANGUAGE_NAME).a lib$(LANGUAGE_NAME).$(SOEXT) 108 | 109 | test: 110 | $(TS) test 111 | 112 | .PHONY: all install uninstall clean test 113 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "TreeSitterJust", 6 | products: [ 7 | .library(name: "TreeSitterJust", targets: ["TreeSitterJust"]), 8 | ], 9 | dependencies: [], 10 | targets: [ 11 | .target(name: "TreeSitterJust", 12 | path: ".", 13 | exclude: [ 14 | "Cargo.toml", 15 | "Makefile", 16 | "binding.gyp", 17 | "bindings/c", 18 | "bindings/go", 19 | "bindings/node", 20 | "bindings/python", 21 | "bindings/rust", 22 | "prebuilds", 23 | "grammar.js", 24 | "package.json", 25 | "package-lock.json", 26 | "pyproject.toml", 27 | "setup.py", 28 | "test", 29 | "examples", 30 | ".editorconfig", 31 | ".github", 32 | ".gitignore", 33 | ".gitattributes", 34 | ".gitmodules", 35 | ], 36 | sources: [ 37 | "src/parser.c", 38 | // NOTE: if your language has an external scanner, add it here. 39 | ], 40 | resources: [ 41 | .copy("queries") 42 | ], 43 | publicHeadersPath: "bindings/swift", 44 | cSettings: [.headerSearchPath("src")]) 45 | ], 46 | cLanguageStandard: .c11 47 | ) 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tree-sitter-just 2 | 3 | Tree-sitter grammar for Justfiles ([casey/just](https://github.com/casey/just)) 4 | 5 | ## Usage 6 | 7 | ### Neovim 8 | 9 | Just queries are now part of `nvim-treesitter`! Please follow installation 10 | instructions at . 11 | 12 | ### Helix 13 | 14 | Just queries are also part of [Helix](https://github.com/helix-editor/helix)! 15 | They should be installed by default. 16 | 17 | ### Manual Installation (Neovim) 18 | 19 | This grammar can also be used locally, as a plugin with Plug, Packer, or a 20 | manual clone. This also requires a simple `ftdetect` plugin for detecting 21 | justfiles. 22 | 23 | Packer: 24 | 25 | ```lua 26 | use "IndianBoy42/tree-sitter-just" 27 | ``` 28 | 29 | Plug 30 | 31 | ```vimscript 32 | Plug 'IndianBoy42/tree-sitter-just' 33 | ``` 34 | 35 | Manual 36 | 37 | ``` 38 | git clone https://github.com/IndianBoy42/tree-sitter-just \ 39 | ~/.local/share/nvim/site/pack/tree-sitter-queries/start/tree-sitter-just 40 | ``` 41 | 42 | You can then do `require('tree-sitter-just').setup({})` to register the parser 43 | with tree-sitter. You can then do `TSInstall`/`TSUpdate` as usual to install 44 | the parser 45 | 46 | You can also add the parser manually using (This is similar to what is done in 47 | `require"tree-sitter-just".setup({})`) 48 | 49 | ```lua 50 | require("nvim-treesitter.parsers").get_parser_configs().just = { 51 | install_info = { 52 | url = "https://github.com/IndianBoy42/tree-sitter-just", -- local path or git repo 53 | files = { "src/parser.c", "src/scanner.c" }, 54 | branch = "main", 55 | -- use_makefile = true -- this may be necessary on MacOS (try if you see compiler errors) 56 | }, 57 | maintainers = { "@IndianBoy42" }, 58 | } 59 | ``` 60 | 61 | Don't forget to `:TSInstall` after adding this. With this method you do not 62 | have to add this repo as a plugin. 63 | 64 | Please note that the nightly version of `nvim-treesitter`, at least since 65 | `5b90ea2abaa4303b9205b5c9002a8cdd0acd11a5` (2024-01-19) is required. 66 | 67 | ### Manual Installation (Helix) 68 | 69 | To use the latest version of this repository with Helix, you just need to 70 | update the `languages.toml` file, often at `~/.config/helix/languages.toml`: 71 | 72 | ```toml 73 | [[grammar]] 74 | name = "just" 75 | # Use this line to use a local dependency 76 | source = { path = "/path/toa/tree-sitter-just" } 77 | # Use this line to use a git dependency 78 | # source = { git = "https://github.com/IndianBoy42/tree-sitter-just", rev = "7ad370abd9eb80e9e5266ea04a1e77d5b1692321" } 79 | 80 | [[language]] 81 | name = "just" 82 | scope = "scope.just" 83 | file-types = ["justfile", "JUSTFILE", "just"] 84 | ``` 85 | 86 | After updating `languages.toml`, run `hx --grammar build` to update. 87 | 88 | To get syntax highlighting and other query-based features you will need to 89 | place the queries in a `queries/just` directory within your runtime folder. 90 | To link to a cloned repository, the following should work: 91 | 92 | ```sh 93 | # Change the second path to your runtime directory if not at `~/.config/helix/runtime` 94 | ln -s $(realpath queries-flavored/helix/) ~/.config/helix/runtime/queries/just 95 | ``` 96 | 97 | ## Contributing 98 | 99 | The easiest way to get started is, fittingly, with 100 | [`just`](https://github.com/casey/just). 101 | 102 | ```sh 103 | # Make sure dependencies are available 104 | just setup 105 | 106 | # Create autogenerated files and run tests 107 | just test 108 | 109 | # Create autogenerated files without testing 110 | just gen 111 | 112 | # Check linting rules 113 | just lint 114 | 115 | # Apply formatting 116 | just fmt 117 | ``` 118 | 119 | Note that `just lint` and `just fmt` must pass for all changes. You can verify 120 | these automatically before committing by running `just pre-commit-install`. 121 | 122 | All our queries are in `queries-src`. This directory is what gets tested by 123 | tree-sitter, and should be usable by helix. To generate queries for NeoVim, 124 | run `./build-flavored-queries.py` (this is run as part of `npm run gen`). 125 | 126 | You can use the [`:InspectTree`](https://neovim.io/doc/user/treesitter.html#%3AInspectTree) 127 | command to explore the resulting parse tree, and 128 | [`:Inspect`](https://neovim.io/doc/user/lua.html#%3AInspect) to view highlight 129 | groups. 130 | 131 | ## Quirks of Just 132 | 133 | Just currently doesn't seem to support comments between attributes or within if 134 | statements, so we do not either. 135 | 136 | ```just 137 | [private] 138 | # hello! 139 | [no-cd] 140 | foo: 141 | ``` 142 | 143 | ```just 144 | foo := if true { 145 | # nope! 146 | "abcd" 147 | } 148 | ``` 149 | 150 | ## Test Information 151 | 152 | The tests directory contains "corpus" tests that are checked for syntax, as 153 | well as "highlight" tests that check the result. The "highlight" test directory 154 | includes some test files generated by the fuzzer that aren't always human 155 | readable. 156 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [ 3 | { 4 | "target_name": "tree_sitter_just_binding", 5 | "dependencies": [ 6 | " 5 | #include 6 | #include 7 | #include 8 | 9 | #define DEFAULT_ALLOC_SIZE 1024; 10 | 11 | TSLanguage *tree_sitter_just(void); 12 | 13 | int main(int argc, char **argv) { 14 | TSParser *parser = ts_parser_new(); 15 | ts_parser_set_language(parser, tree_sitter_just()); 16 | 17 | assert(argc == 2 && "must provide a file name"); 18 | 19 | if (argc < 2) { 20 | printf("must provide one or more file names\n"); 21 | return 1; 22 | } 23 | 24 | size_t alloc_size = DEFAULT_ALLOC_SIZE; 25 | char *data = malloc(alloc_size); 26 | assert(data); 27 | 28 | for (int i = 1; i < argc; ++i) { 29 | memset(data, 0, alloc_size); 30 | 31 | FILE *fp = fopen(argv[i], "r"); 32 | if (!fp) { 33 | printf("failed to open file %s\n", argv[i]); 34 | exit(1); 35 | } 36 | 37 | fseek(fp, 0L, SEEK_END); 38 | size_t file_size = ftell(fp); 39 | rewind(fp); 40 | 41 | if (file_size > alloc_size) { 42 | data = realloc(data, file_size); 43 | assert(data); 44 | alloc_size = file_size; 45 | } 46 | 47 | size_t readlen = fread(data, sizeof(char), alloc_size, fp); 48 | assert(readlen == file_size); 49 | assert(!ferror(fp)); 50 | printf("read %zu bytes\n", readlen); 51 | 52 | // Build a syntax tree based on source code stored in a string. 53 | TSTree *tree = ts_parser_parse_string(parser, NULL, data, file_size); 54 | // TSNode root_node = ts_tree_root_node(tree); 55 | // assert(ts_node_child_count(root_node) > 0); 56 | 57 | // Free all of the heap-allocated memory. 58 | ts_tree_delete(tree); 59 | ts_parser_delete(parser); 60 | } 61 | 62 | free(data); 63 | 64 | return 0; 65 | } 66 | -------------------------------------------------------------------------------- /bindings/go/binding.go: -------------------------------------------------------------------------------- 1 | package tree_sitter_just 2 | 3 | // #cgo CFLAGS: -std=c11 -fPIC 4 | // #include "../../src/parser.c" 5 | // // NOTE: if your language has an external scanner, add it here. 6 | import "C" 7 | 8 | import "unsafe" 9 | 10 | // Get the tree-sitter Language for this grammar. 11 | func Language() unsafe.Pointer { 12 | return unsafe.Pointer(C.tree_sitter_just()) 13 | } 14 | -------------------------------------------------------------------------------- /bindings/go/binding_test.go: -------------------------------------------------------------------------------- 1 | package tree_sitter_just_test 2 | 3 | import ( 4 | "testing" 5 | 6 | tree_sitter "github.com/smacker/go-tree-sitter" 7 | "github.com/tree-sitter/tree-sitter-just" 8 | ) 9 | 10 | func TestCanLoadGrammar(t *testing.T) { 11 | language := tree_sitter.NewLanguage(tree_sitter_just.Language()) 12 | if language == nil { 13 | t.Errorf("Error loading Just grammar") 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /bindings/go/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tree-sitter/tree-sitter-just 2 | 3 | go 1.22 4 | 5 | require github.com/smacker/go-tree-sitter v0.0.0-20230720070738-0d0a9f78d8f8 6 | -------------------------------------------------------------------------------- /bindings/node/binding.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | typedef struct TSLanguage TSLanguage; 4 | 5 | extern "C" TSLanguage *tree_sitter_just(); 6 | 7 | // "tree-sitter", "language" hashed with BLAKE2 8 | const napi_type_tag LANGUAGE_TYPE_TAG = { 9 | 0x8AF2E5212AD58ABF, 0xD5006CAD83ABBA16 10 | }; 11 | 12 | Napi::Object Init(Napi::Env env, Napi::Object exports) { 13 | exports["name"] = Napi::String::New(env, "just"); 14 | auto language = Napi::External::New(env, tree_sitter_just()); 15 | language.TypeTag(&LANGUAGE_TYPE_TAG); 16 | exports["language"] = language; 17 | return exports; 18 | } 19 | 20 | NODE_API_MODULE(tree_sitter_just_binding, Init) 21 | -------------------------------------------------------------------------------- /bindings/node/index.d.ts: -------------------------------------------------------------------------------- 1 | type BaseNode = { 2 | type: string; 3 | named: boolean; 4 | }; 5 | 6 | type ChildNode = { 7 | multiple: boolean; 8 | required: boolean; 9 | types: BaseNode[]; 10 | }; 11 | 12 | type NodeInfo = 13 | | (BaseNode & { 14 | subtypes: BaseNode[]; 15 | }) 16 | | (BaseNode & { 17 | fields: { [name: string]: ChildNode }; 18 | children: ChildNode[]; 19 | }); 20 | 21 | type Language = { 22 | name: string; 23 | language: unknown; 24 | nodeTypeInfo: NodeInfo[]; 25 | }; 26 | 27 | declare const language: Language; 28 | export = language; 29 | -------------------------------------------------------------------------------- /bindings/node/index.js: -------------------------------------------------------------------------------- 1 | const root = require("path").join(__dirname, "..", ".."); 2 | 3 | module.exports = require("node-gyp-build")(root); 4 | 5 | try { 6 | module.exports.nodeTypeInfo = require("../../src/node-types.json"); 7 | } catch (_) {} 8 | -------------------------------------------------------------------------------- /bindings/python/tree_sitter_just/__init__.py: -------------------------------------------------------------------------------- 1 | "Just grammar for tree-sitter" 2 | 3 | from ._binding import language 4 | 5 | __all__ = ["language"] 6 | -------------------------------------------------------------------------------- /bindings/python/tree_sitter_just/__init__.pyi: -------------------------------------------------------------------------------- 1 | def language() -> int: ... 2 | -------------------------------------------------------------------------------- /bindings/python/tree_sitter_just/binding.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | typedef struct TSLanguage TSLanguage; 4 | 5 | TSLanguage *tree_sitter_just(void); 6 | 7 | static PyObject *_binding_language(PyObject *self, PyObject *args) { 8 | return PyLong_FromVoidPtr(tree_sitter_just()); 9 | } 10 | 11 | static PyMethodDef methods[] = { 12 | {"language", _binding_language, METH_NOARGS, 13 | "Get the tree-sitter language for this grammar."}, 14 | {NULL, NULL, 0, NULL}}; 15 | 16 | static struct PyModuleDef module = {.m_base = PyModuleDef_HEAD_INIT, 17 | .m_name = "_binding", 18 | .m_doc = NULL, 19 | .m_size = -1, 20 | .m_methods = methods}; 21 | 22 | PyMODINIT_FUNC PyInit__binding(void) { return PyModule_Create(&module); } 23 | -------------------------------------------------------------------------------- /bindings/python/tree_sitter_just/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IndianBoy42/tree-sitter-just/bb0c898a80644de438e6efe5d88d30bf092935cd/bindings/python/tree_sitter_just/py.typed -------------------------------------------------------------------------------- /bindings/rust/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | let src_dir = std::path::Path::new("src"); 3 | 4 | let mut c_config = cc::Build::new(); 5 | c_config.include(src_dir); 6 | c_config 7 | .flag_if_supported("-Wno-unused-parameter") 8 | .flag_if_supported("-Wno-unused-but-set-variable") 9 | .flag_if_supported("-Wno-trigraphs"); 10 | 11 | #[cfg(target_env = "msvc")] 12 | c_config.flag("-utf-8"); 13 | 14 | let parser_path = src_dir.join("parser.c"); 15 | println!("cargo:rerun-if-changed={}", parser_path.to_str().unwrap()); 16 | c_config.file(&parser_path); 17 | 18 | let scanner_path = src_dir.join("scanner.c"); 19 | println!("cargo:rerun-if-changed={}", scanner_path.to_str().unwrap()); 20 | c_config.file(&scanner_path); 21 | 22 | c_config.compile("parser"); 23 | } 24 | -------------------------------------------------------------------------------- /bindings/rust/lib.rs: -------------------------------------------------------------------------------- 1 | //! This crate provides just language support for the [tree-sitter][] parsing library. 2 | //! 3 | //! Typically, you will use the [language][language func] function to add this language to a 4 | //! tree-sitter [Parser][], and then use the parser to parse some code: 5 | //! 6 | //! ``` 7 | //! let code = ""; 8 | //! let mut parser = tree_sitter::Parser::new(); 9 | //! parser.set_language(&tree_sitter_just::language()).expect("Error loading just grammar"); 10 | //! let tree = parser.parse(code, None).unwrap(); 11 | //! ``` 12 | //! 13 | //! [Language]: https://docs.rs/tree-sitter/*/tree_sitter/struct.Language.html 14 | //! [language func]: fn.language.html 15 | //! [Parser]: https://docs.rs/tree-sitter/*/tree_sitter/struct.Parser.html 16 | //! [tree-sitter]: https://tree-sitter.github.io/ 17 | 18 | use tree_sitter::Language; 19 | 20 | extern "C" { 21 | fn tree_sitter_just() -> Language; 22 | } 23 | 24 | /// Get the tree-sitter [Language][] for this grammar. 25 | /// 26 | /// [Language]: https://docs.rs/tree-sitter/*/tree_sitter/struct.Language.html 27 | pub fn language() -> Language { 28 | unsafe { tree_sitter_just() } 29 | } 30 | 31 | /// The content of the [`node-types.json`][] file for this grammar. 32 | /// 33 | /// [`node-types.json`]: https://tree-sitter.github.io/tree-sitter/using-parsers#static-node-types 34 | pub const NODE_TYPES: &str = include_str!("../../src/node-types.json"); 35 | 36 | pub const HIGHLIGHTS_QUERY: &str = include_str!("../../queries-flavored/helix/highlights.scm"); 37 | pub const INJECTIONS_QUERY: &str = include_str!("../../queries-flavored/helix/injections.scm"); 38 | pub const LOCALS_QUERY: &str = include_str!("../../queries-flavored/helix/locals.scm"); 39 | 40 | // FIXME: add tags when available 41 | // pub const TAGS_QUERY: &'static str = include_str!("../../queries-src/tags.scm"); 42 | 43 | #[cfg(test)] 44 | mod tests { 45 | #[test] 46 | fn test_can_load_grammar() { 47 | let mut parser = tree_sitter::Parser::new(); 48 | parser 49 | .set_language(&super::language()) 50 | .expect("Error loading just language"); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /bindings/swift/TreeSitterJust/just.h: -------------------------------------------------------------------------------- 1 | #ifndef TREE_SITTER_JUST_H_ 2 | #define TREE_SITTER_JUST_H_ 3 | 4 | typedef struct TSLanguage TSLanguage; 5 | 6 | #ifdef __cplusplus 7 | extern "C" { 8 | #endif 9 | 10 | const TSLanguage *tree_sitter_just(void); 11 | 12 | #ifdef __cplusplus 13 | } 14 | #endif 15 | 16 | #endif // TREE_SITTER_JUST_H_ 17 | -------------------------------------------------------------------------------- /build-flavored-queries.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Generate NeoVim queries. 3 | 4 | Everything in `queries/` uses tree-sitter syntax, as defined at 5 | . However, each 6 | editor has a slightly different syntax. 7 | 8 | This file performs conversions so two sets of files don't need to be maintained. 9 | 10 | has a bit better 11 | guide for the standard parameters than tree-sitter does. 12 | """ 13 | 14 | import re 15 | from glob import glob 16 | from pathlib import Path 17 | 18 | REPLACEMENTS_HELIX = [] 19 | REPLACEMENTS_ZED = [] 20 | REPLACEMENTS_LAPCE = [] 21 | 22 | ALLOWED_CAPS_ALL = ["@error"] 23 | ALLOWED_PREDICATES_ALL = [ 24 | "#eq?", 25 | "#match?", 26 | ] 27 | 28 | ALLOWED_CAPS_NVIM = { 29 | "highlights.scm": [ 30 | "@variable", 31 | "@variable.builtin", 32 | "@variable.parameter", 33 | "@variable.parameter.builtin", 34 | "@variable.member", 35 | "@constant", 36 | "@constant.builtin", 37 | "@constant.macro", 38 | "@module", 39 | "@module.builtin", 40 | "@label", 41 | "@string", 42 | "@string.documentation", 43 | "@string.regexp", 44 | "@string.escape", 45 | "@string.special", 46 | "@string.special.symbol", 47 | "@string.special.url", 48 | "@string.special.path", 49 | "@character", 50 | "@character.special", 51 | "@boolean", 52 | "@number", 53 | "@number.float", 54 | "@type", 55 | "@type.builtin", 56 | "@type.definition", 57 | "@type.qualifier", 58 | "@attribute", 59 | "@attribute.builtin", 60 | "@property", 61 | "@function", 62 | "@function.builtin", 63 | "@function.call", 64 | "@function.macro", 65 | "@function.method", 66 | "@function.method.call", 67 | "@constructor", 68 | "@operator", 69 | "@keyword", 70 | "@keyword.coroutine", 71 | "@keyword.function", 72 | "@keyword.operator", 73 | "@keyword.import", 74 | "@keyword.storage", 75 | "@keyword.type", 76 | "@keyword.repeat", 77 | "@keyword.return", 78 | "@keyword.debug", 79 | "@keyword.exception", 80 | "@keyword.conditional", 81 | "@keyword.conditional.ternary", 82 | "@keyword.directive", 83 | "@keyword.directive.define", 84 | "@punctuation.delimiter", 85 | "@punctuation.bracket", 86 | "@punctuation.special", 87 | "@comment", 88 | "@comment.documentation", 89 | "@comment.error", 90 | "@comment.warning", 91 | "@comment.todo", 92 | "@comment.note", 93 | "@none", 94 | "@conceal", 95 | "@spell", 96 | "@nospell", 97 | ], 98 | "injections.scm": [ 99 | "@injection.language", 100 | "@injection.content", 101 | "@comment", 102 | ], 103 | "locals.scm": [ 104 | "@local.definition", 105 | "@local.definition.constant", 106 | "@local.definition.function", 107 | "@local.definition.method", 108 | "@local.definition.var", 109 | "@local.definition.parameter", 110 | "@local.definition.macro", 111 | "@local.definition.type", 112 | "@local.definition.field", 113 | "@local.definition.enum", 114 | "@local.definition.namespace", 115 | "@local.definition.import", 116 | "@local.definition.associated", 117 | "@local.scope", 118 | "@local.reference", 119 | ], 120 | "folds.scm": [ 121 | "@fold", 122 | ], 123 | "indents.scm": [ 124 | "@indent.begin", 125 | "@indent.end", 126 | "@indent.align", 127 | "@indent.dedent", 128 | "@indent.branch", 129 | "@indent.ignore", 130 | "@indent.auto", 131 | "@indent.zero", 132 | ], 133 | "textobjects.scm": [ 134 | "@attribute.inner", 135 | "@attribute.outer", 136 | "@function.inner", 137 | "@function.outer", 138 | "@class.inner", 139 | "@class.outer", 140 | "@conditional.inner", 141 | "@conditional.outer", 142 | "@loop.inner", 143 | "@loop.outer", 144 | "@call.inner", 145 | "@call.outer", 146 | "@block.inner", 147 | "@block.outer", 148 | "@parameter.inner", 149 | "@parameter.outer", 150 | "@regex.inner", 151 | "@regex.outer", 152 | "@comment.inner", 153 | "@comment.outer", 154 | "@assignment.inner", 155 | "@assignment.outer", 156 | "@return.inner", 157 | "@return.outer", 158 | "@frame.inner", 159 | "@frame.outer", 160 | ], 161 | } 162 | 163 | ALLOWED_SETTINGS_NVIM = { 164 | "injections.scm": [ 165 | "injection.combined", 166 | "injection.language", 167 | "injection.include-children", 168 | ] 169 | } 170 | 171 | # Old nvim-treesitter before updates 172 | REPLACEMENTS_NVIM = [ 173 | # Changes to highlight queries 174 | (r"@comment.line", r"@comment"), 175 | (r"@constant.builtin.boolean", r"@boolean"), 176 | (r"@constant.character.escape", r"@string.escape"), 177 | (r"@keyword.module", r"@module"), 178 | (r"@keyword.control.", r"@keyword."), 179 | (r"@namespace", r"@module"), 180 | # Changes to indent queries 181 | (r"@indent\s+@extend", r"@indent.begin"), 182 | # Changes to textobject queries 183 | (r"(@[\w.]+\.)inside", r"\1inner"), 184 | (r"(@[\w.]+\.)around", r"\1outer"), 185 | # nvim uses `var` rather than `variable` 186 | (r"(@[\w.]+)\.variable", r"\1.var"), 187 | # nothing more specific than reference 188 | (r"(@local.reference)[\w.]+", r"\1"), 189 | ] 190 | 191 | ALLOWED_CAPS_HELIX = { 192 | "highlights.scm": [ 193 | "@attribute", 194 | "@type.builtin", 195 | "@type.parameter", 196 | "@type.enum.variant", 197 | "@constructor", 198 | "@constant.builtin", 199 | "@constant.builtin.boolean", 200 | "@constant.character", 201 | "@constant.character.escape", 202 | "@constant.numeric", 203 | "@constant.numeric.integer", 204 | "@constant.numeric.float", 205 | "@string", 206 | "@string.regexp", 207 | "@string.special.path", 208 | "@string.special.url", 209 | "@string.special.symbol", 210 | "@comment", 211 | "@comment.line", 212 | "@comment.block", 213 | "@comment.block.documentation", 214 | "@variable", 215 | "@variable.builtin", 216 | "@variable.parameter", 217 | "@variable.other", 218 | "@variable.other.member", 219 | "@label", 220 | "@punctuation", 221 | "@punctuation.delimiter", 222 | "@punctuation.bracket", 223 | "@punctuation.special", 224 | "@keyword", 225 | "@keyword.control", 226 | "@keyword.control.conditional", 227 | "@keyword.control.repeat", 228 | "@keyword.control.import", 229 | "@keyword.control.return", 230 | "@keyword.control.exception", 231 | "@keyword.operator", 232 | "@keyword.directive", 233 | "@keyword.function", 234 | "@keyword.storage", 235 | "@keyword.storage.type", 236 | "@keyword.storage.modifier", 237 | "@operator", 238 | "@function", 239 | "@function.builtin", 240 | "@function.method", 241 | "@function.macro", 242 | "@function.special", 243 | "@namespace", 244 | "@special", 245 | ], 246 | "injections.scm": [ 247 | "@injection.language", 248 | "@injection.content", 249 | "@injection.filename", 250 | "@injection.shebang", 251 | "@comment", 252 | ], 253 | # unspecified in helix 254 | "locals.scm": [ 255 | c.replace("var", "variable") for c in ALLOWED_CAPS_NVIM["locals.scm"] 256 | ], 257 | "folds.scm": ALLOWED_CAPS_NVIM["folds.scm"], 258 | "indents.scm": [ 259 | "@indent", 260 | "@outdent", 261 | "@indent.always", 262 | "@outdent.always", 263 | "@align", 264 | "@extend", 265 | "@extend.prevent-once", 266 | ], 267 | "textobjects.scm": [ 268 | "@function.inside", 269 | "@function.around", 270 | "@class.inside", 271 | "@class.around", 272 | "@test.inside", 273 | "@test.around", 274 | "@parameter.inside", 275 | "@parameter.around", 276 | "@comment.inside", 277 | "@comment.around", 278 | ], 279 | } 280 | 281 | ALLOWED_SETTINGS_HELIX = { 282 | "injections.scm": [ 283 | "injection.combined", 284 | "injection.language", 285 | "injection.include-children", 286 | "injection.include-unnamed-children", 287 | ] 288 | } 289 | 290 | REPLACEMENTS_HELIX = [ 291 | (r"@keyword.module", r"@keyword.directive"), 292 | (r"@function.call", r"@function"), 293 | (r"@spell ?", r""), 294 | # nothing more specific than `@local.(reference,definition)` 295 | (r"(@local\.\w+)\.[.\w]+", r"\1"), 296 | ] 297 | 298 | # Documentation not yet complete 299 | ALLOWED_CAPS_ZED = ALLOWED_CAPS_HELIX 300 | ALLOWED_SETTINGS_ZED = ALLOWED_SETTINGS_HELIX 301 | REPLACEMENTS_ZED = REPLACEMENTS_HELIX 302 | ALLOWED_CAPS_LAPCE = ALLOWED_CAPS_HELIX 303 | ALLOWED_SETTINGS_LAPCE = ALLOWED_SETTINGS_HELIX 304 | REPLACEMENTS_LAPCE = REPLACEMENTS_HELIX 305 | 306 | # (rname, eplacements, allowed caps, base path) mappings to create a flavor 307 | FLAVOR_MAPPINGS = [ 308 | ( 309 | "new NeoVim", 310 | "NVIM", 311 | REPLACEMENTS_NVIM, 312 | ALLOWED_CAPS_NVIM, 313 | ALLOWED_SETTINGS_NVIM, 314 | Path("queries") / "just", 315 | ), 316 | ( 317 | "Helix", 318 | "HELIX", 319 | REPLACEMENTS_HELIX, 320 | ALLOWED_CAPS_HELIX, 321 | ALLOWED_SETTINGS_HELIX, 322 | Path("queries-flavored") / "helix", 323 | ), 324 | ( 325 | "Zed", 326 | "ZED", 327 | REPLACEMENTS_ZED, 328 | ALLOWED_CAPS_ZED, 329 | ALLOWED_SETTINGS_ZED, 330 | Path("queries-flavored") / "zed", 331 | ), 332 | ( 333 | "Lapce", 334 | "LAPCE", 335 | REPLACEMENTS_LAPCE, 336 | ALLOWED_CAPS_LAPCE, 337 | ALLOWED_SETTINGS_LAPCE, 338 | Path("queries-flavored") / "lapce", 339 | ), 340 | ] 341 | 342 | 343 | def main(): 344 | sources = glob("queries-src/*.scm") 345 | for fname in sources: 346 | qname = Path(fname).name 347 | print(f"Generating flavored queries from {fname}") 348 | base_contents = "; File autogenerated by build-queries-nvim.py; do not edit\n\n" 349 | 350 | with open(fname) as f: 351 | base_contents += f.read() 352 | 353 | for ( 354 | name, 355 | tag, 356 | replacements, 357 | allowed_caps, 358 | allowed_settings, 359 | basepath, 360 | ) in FLAVOR_MAPPINGS: 361 | # Remove lines as indicated by directives 362 | contents = "\n".join( 363 | ( 364 | line 365 | for line in base_contents.splitlines() 366 | if f"SKIP-{tag}" not in line 367 | ) 368 | ) 369 | 370 | # Delete other directives 371 | contents = re.sub(r";?\s*SKIP-[\w-]+", "", contents) 372 | 373 | for rep in replacements: 374 | pat = rep[0] 375 | sub = rep[1] 376 | flags = 0 377 | if len(rep) > 2: 378 | flags = rep[2] 379 | contents = re.sub(pat, sub, contents, flags=flags) 380 | 381 | # Remove trailing whitespace and duplicate newlines 382 | contents = re.sub(r"[\s;]+$", "", contents) 383 | contents = "\n".join((line.rstrip() for line in contents.splitlines())) 384 | contents = re.sub(r"((?:\r?\n){2,})(?:\r?\n)+", r"\1", contents) 385 | 386 | if not contents.endswith("\n"): 387 | contents += "\n" 388 | 389 | basepath.mkdir(parents=True, exist_ok=True) 390 | dest = basepath / Path(fname).name 391 | 392 | with open(dest, "w") as f: 393 | f.write(contents) 394 | 395 | # Validate all captures are valid 396 | for qcap in re.finditer(r"@[\w.]+", contents): 397 | matched = qcap[0] 398 | 399 | # Internal captures 400 | if matched.startswith("@_"): 401 | continue 402 | 403 | allowed = allowed_caps[qname] 404 | assert ( 405 | matched in allowed or matched in ALLOWED_CAPS_ALL 406 | ), f"found disallowed query '{matched}' in '{dest}' ({name}). Allowed: {allowed}" 407 | 408 | # Validate all settings are valid 409 | for qsetting in re.finditer(r"#set!\s+([\w.-]+)", contents): 410 | matched = qsetting[1] 411 | allowed = allowed_settings[qname] 412 | assert ( 413 | matched in allowed 414 | ), f"found disallowed setting '{qsetting[0]}' in '{dest}' ({name}). Allowed: {allowed}" 415 | 416 | 417 | if __name__ == "__main__": 418 | main() 419 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import google from "eslint-config-google"; 3 | import prettier from "eslint-config-prettier"; 4 | 5 | export default [ 6 | { 7 | ignores: ["repositories/", "target/"], 8 | }, 9 | prettier, 10 | { 11 | ...google, 12 | languageOptions: { 13 | globals: { 14 | ...globals.commonjs, 15 | ...globals.es2021, 16 | }, 17 | 18 | ecmaVersion: "latest", 19 | sourceType: "module", 20 | }, 21 | 22 | rules: { 23 | indent: [ 24 | "error", 25 | 2, 26 | { 27 | SwitchCase: 1, 28 | }, 29 | ], 30 | 31 | "max-len": [ 32 | "error", 33 | { 34 | code: 120, 35 | ignoreComments: true, 36 | ignoreUrls: true, 37 | ignoreStrings: true, 38 | }, 39 | ], 40 | }, 41 | }, 42 | ]; 43 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1710146030, 9 | "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1719468428, 24 | "narHash": "sha256-vN5xJAZ4UGREEglh3lfbbkIj+MPEYMuqewMn4atZFaQ=", 25 | "owner": "nixos", 26 | "repo": "nixpkgs", 27 | "rev": "1e3deb3d8a86a870d925760db1a5adecc64d329d", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "nixos", 32 | "ref": "nixpkgs-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "root": { 38 | "inputs": { 39 | "flake-utils": "flake-utils", 40 | "nixpkgs": "nixpkgs" 41 | } 42 | }, 43 | "systems": { 44 | "locked": { 45 | "lastModified": 1681028828, 46 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 47 | "owner": "nix-systems", 48 | "repo": "default", 49 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 50 | "type": "github" 51 | }, 52 | "original": { 53 | "owner": "nix-systems", 54 | "repo": "default", 55 | "type": "github" 56 | } 57 | } 58 | }, 59 | "root": "root", 60 | "version": 7 61 | } 62 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; 4 | 5 | flake-utils.url = "github:numtide/flake-utils"; 6 | }; 7 | 8 | outputs = { nixpkgs, flake-utils, ... }: 9 | flake-utils.lib.eachDefaultSystem (system: 10 | let 11 | pkgs = import nixpkgs { inherit system; }; 12 | in 13 | { 14 | devShells.default = with pkgs; 15 | let 16 | darwinInclude = lib.optionalString stdenv.isDarwin ''- "-I${darwin.Libsystem}/include/"''; 17 | in 18 | mkShell { 19 | packages = [ tree-sitter nodejs-slim_22 graphviz ]; 20 | shellHook = '' 21 | cat < .clangd 22 | CompileFlags: 23 | Add: 24 | - "-I${clang}/resource-root/include/" 25 | ${darwinInclude} 26 | EOF 27 | ''; 28 | }; 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /ftdetect/just.vim: -------------------------------------------------------------------------------- 1 | au VimEnter,BufWinEnter,BufRead,BufNewFile {.,}justfile\c,*.just setlocal filetype=just | setlocal commentstring=#\ %s 2 | -------------------------------------------------------------------------------- /fuzzer/build-corpus.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Turn our tests into a corpus for the fuzzer (one test per file)""" 3 | 4 | from pathlib import Path 5 | import re 6 | 7 | RE = r"===+\n(?P.*?)\n===+\n(?P.*?)\n---+" 8 | REPO = Path(__file__).parent.parent 9 | OUT_DIR = REPO / "fuzzer" / "corpus" 10 | 11 | 12 | def main(): 13 | ts_corpus_files = (REPO / "test" / "corpus").glob("*.txt") 14 | 15 | corpus = {} 16 | 17 | for fname in ts_corpus_files: 18 | text = fname.read_text() 19 | prefix = fname.name.rstrip(".txt") 20 | 21 | for match in re.finditer(RE, text, re.MULTILINE | re.DOTALL): 22 | name = match.group("name").replace(" ", "_") 23 | name = f"{prefix}_{name}.just" 24 | source = match.group("source") 25 | corpus[name] = source 26 | 27 | OUT_DIR.mkdir(exist_ok=True) 28 | 29 | # Clear the corpus of all files we created 30 | for existing in OUT_DIR.iterdir(): 31 | if existing.name.endswith(".just"): 32 | existing.unlink() 33 | 34 | for name, source in corpus.items(): 35 | out_file: Path = OUT_DIR / name 36 | out_file.write_text(source) 37 | 38 | 39 | if __name__ == "__main__": 40 | main() 41 | -------------------------------------------------------------------------------- /fuzzer/entry.c: -------------------------------------------------------------------------------- 1 | #include "tree_sitter/api.h" 2 | #include 3 | #include 4 | 5 | TSLanguage *tree_sitter_just(void); 6 | 7 | int LLVMFuzzerTestOneInput(const uint8_t *data, const size_t len) { 8 | TSParser *parser = ts_parser_new(); 9 | ts_parser_set_language(parser, tree_sitter_just()); 10 | 11 | // Build a syntax tree based on source code stored in a string. 12 | TSTree *tree = ts_parser_parse_string(parser, NULL, (const char *)data, len); 13 | // Free all of the heap-allocated memory. 14 | ts_tree_delete(tree); 15 | ts_parser_delete(parser); 16 | return 0; 17 | } 18 | -------------------------------------------------------------------------------- /grammar.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Justfile grammar for tree-sitter 3 | * @author Anshuman Medhi 4 | * @author Trevor Gross 5 | * @author Amaan Qureshi 6 | * @license Apache-2.0 7 | */ 8 | 9 | /// 10 | // @ts-check 11 | 12 | const ESCAPE_SEQUENCE = token(/\\([nrt"\\]|(\r?\n))/); 13 | // Flags to `/usr/bin/env`, anything that starts with a dash 14 | const SHEBANG_ENV_FLAG = token(/-\S*/); 15 | 16 | /** 17 | * Creates a rule to match one or more of the rules separated by a comma 18 | * 19 | * @param {RuleOrLiteral} rule 20 | * 21 | * @return {SeqRule} 22 | */ 23 | function comma_sep1(rule) { 24 | return seq(rule, repeat(seq(",", rule))); 25 | } 26 | 27 | /** 28 | * Creates a rule to match an array-like structure filled with `item` 29 | * 30 | * @param {RuleOrLiteral} rule 31 | * 32 | * @return {Rule} 33 | */ 34 | function array(rule) { 35 | const item = field("element", rule); 36 | return field( 37 | "array", 38 | seq( 39 | "[", 40 | optional(field("content", seq(comma_sep1(item), optional(item)))), 41 | "]", 42 | ), 43 | ); 44 | } 45 | 46 | module.exports = grammar({ 47 | name: "just", 48 | 49 | externals: ($) => [ 50 | $._indent, 51 | $._dedent, 52 | $._newline, 53 | $.text, 54 | $.error_recovery, 55 | ], 56 | 57 | // Allow comments, backslash-escaped newlines (with optional trailing whitespace), 58 | // and whitespace anywhere 59 | extras: ($) => [$.comment, /\\(\n|\r\n)\s*/, /\s/], 60 | 61 | inline: ($) => [ 62 | $._string, 63 | $._string_indented, 64 | $._raw_string_indented, 65 | $._expression_recurse, 66 | ], 67 | word: ($) => $.identifier, 68 | 69 | rules: { 70 | // justfile : item* EOF 71 | source_file: ($) => 72 | seq(optional(seq($.shebang, $._newline)), repeat($._item)), 73 | 74 | // item : recipe 75 | // | alias 76 | // | assignment 77 | // | export 78 | // | import 79 | // | module 80 | // | setting 81 | _item: ($) => 82 | choice( 83 | $.recipe, 84 | $.alias, 85 | $.assignment, 86 | $.export, 87 | $.import, 88 | $.module, 89 | $.setting, 90 | ), 91 | 92 | // alias : 'alias' NAME ':=' NAME 93 | alias: ($) => 94 | seq( 95 | repeat($.attribute), 96 | "alias", 97 | field("left", $.identifier), 98 | ":=", 99 | field("right", $.identifier), 100 | ), 101 | // assignment : NAME ':=' expression _eol 102 | assignment: ($) => 103 | seq( 104 | field("left", $.identifier), 105 | ":=", 106 | field("right", $.expression), 107 | $._newline, 108 | ), 109 | 110 | // export : 'export' assignment 111 | export: ($) => seq("export", $.assignment), 112 | 113 | // import : 'import' '?'? string? 114 | import: ($) => seq("import", optional("?"), $.string), 115 | 116 | // module : 'mod' '?'? string? 117 | module: ($) => 118 | seq( 119 | "mod", 120 | optional("?"), 121 | field("name", $.identifier), 122 | optional($.string), 123 | ), 124 | 125 | // setting : 'set' 'dotenv-load' boolean? 126 | // | 'set' 'export' boolean? 127 | // | 'set' 'positional-arguments' boolean? 128 | // | 'set' 'shell' ':=' '[' string (',' string)* ','? ']' 129 | setting: ($) => 130 | choice( 131 | seq( 132 | "set", 133 | field("left", $.identifier), 134 | field( 135 | "right", 136 | optional(seq(":=", choice($.boolean, $.string, array($.string)))), 137 | ), 138 | $._newline, 139 | ), 140 | seq("set", "shell", ":=", field("right", array($.string)), $._newline), 141 | ), 142 | 143 | // boolean : ':=' ('true' | 'false') 144 | boolean: (_) => choice("true", "false"), 145 | 146 | // expression : 'if' condition '{' expression '}' 'else' '{' expression '}' 147 | // | value '/' expression 148 | // | value '+' expression 149 | // | value 150 | expression: ($) => seq(optional("/"), $._expression_inner), 151 | 152 | _expression_inner: ($) => 153 | choice( 154 | $.if_expression, 155 | prec.left(2, seq($._expression_recurse, "+", $._expression_recurse)), 156 | prec.left(1, seq($._expression_recurse, "/", $._expression_recurse)), 157 | $.value, 158 | ), 159 | 160 | // We can't mark `_expression_inner` inline because it causes an infinite 161 | // loop at generation, so we just alias it. 162 | _expression_recurse: ($) => alias($._expression_inner, "expression"), 163 | 164 | if_expression: ($) => 165 | seq( 166 | "if", 167 | $.condition, 168 | field("consequence", $._braced_expr), 169 | repeat(field("alternative", $.else_if_clause)), 170 | optional(field("alternative", $.else_clause)), 171 | ), 172 | 173 | else_if_clause: ($) => seq("else", "if", $.condition, $._braced_expr), 174 | 175 | else_clause: ($) => seq("else", $._braced_expr), 176 | 177 | _braced_expr: ($) => seq("{", field("body", $.expression), "}"), 178 | 179 | // condition : expression '==' expression 180 | // | expression '!=' expression 181 | // | expression '=~' expression 182 | condition: ($) => 183 | choice( 184 | seq($.expression, "==", $.expression), 185 | seq($.expression, "!=", $.expression), 186 | seq($.expression, "=~", choice($.regex_literal, $.expression)), 187 | // verify whether this is valid 188 | $.expression, 189 | ), 190 | 191 | // Capture this special for injections 192 | regex_literal: ($) => prec(1, $.string), 193 | 194 | // value : NAME '(' sequence? ')' 195 | // | BACKTICK 196 | // | INDENTED_BACKTICK 197 | // | NAME 198 | // | string 199 | // | '(' expression ')' 200 | value: ($) => 201 | prec.left( 202 | choice( 203 | $.function_call, 204 | $.external_command, 205 | $.identifier, 206 | $.string, 207 | $.numeric_error, 208 | seq("(", $.expression, ")"), 209 | ), 210 | ), 211 | 212 | function_call: ($) => 213 | seq( 214 | field("name", $.identifier), 215 | "(", 216 | optional(field("arguments", $.sequence)), 217 | ")", 218 | ), 219 | 220 | external_command: ($) => 221 | choice(seq($._backticked), seq($._indented_backticked)), 222 | 223 | // sequence : expression ',' sequence 224 | // | expression ','? 225 | sequence: ($) => comma_sep1($.expression), 226 | 227 | attribute: ($) => 228 | seq( 229 | "[", 230 | comma_sep1( 231 | choice( 232 | $.identifier, 233 | seq( 234 | $.identifier, 235 | "(", 236 | field("argument", comma_sep1($.string)), 237 | ")", 238 | ), 239 | seq($.identifier, ":", field("argument", $.string)), 240 | ), 241 | ), 242 | "]", 243 | $._newline, 244 | ), 245 | 246 | // A complete recipe 247 | // recipe : attribute? '@'? NAME parameter* variadic_parameters? ':' dependency* body? 248 | recipe: ($) => 249 | seq( 250 | repeat($.attribute), 251 | $.recipe_header, 252 | $._newline, 253 | optional($.recipe_body), 254 | ), 255 | 256 | recipe_header: ($) => 257 | seq( 258 | optional("@"), 259 | field("name", $.identifier), 260 | optional($.parameters), 261 | ":", 262 | optional($.dependencies), 263 | ), 264 | 265 | parameters: ($) => 266 | seq(repeat($.parameter), choice($.parameter, $.variadic_parameter)), 267 | 268 | // FIXME: do we really have leading `$`s here?` 269 | // parameter : '$'? NAME 270 | // | '$'? NAME '=' value 271 | parameter: ($) => 272 | seq( 273 | optional("$"), 274 | field("name", $.identifier), 275 | optional(seq("=", field("default", $.value))), 276 | ), 277 | 278 | // variadic_parameters : '*' parameter 279 | // | '+' parameter 280 | variadic_parameter: ($) => 281 | seq(field("kleene", choice("*", "+")), $.parameter), 282 | 283 | dependencies: ($) => repeat1(seq(optional("&&"), $.dependency)), 284 | 285 | // dependency : NAME 286 | // | '(' NAME expression* ')' 287 | dependency: ($) => 288 | choice(field("name", $.identifier), $.dependency_expression), 289 | 290 | // contents of `(recipe expression)` 291 | dependency_expression: ($) => 292 | seq("(", field("name", $.identifier), repeat($.expression), ")"), 293 | 294 | // body : INDENT line+ DEDENT 295 | recipe_body: ($) => 296 | seq( 297 | $._indent, 298 | optional(seq(field("shebang", $.shebang), $._newline)), 299 | repeat(choice(seq($.recipe_line, $._newline), $._newline)), 300 | $._dedent, 301 | ), 302 | 303 | recipe_line: ($) => 304 | seq( 305 | optional($.recipe_line_prefix), 306 | repeat1(choice($.text, $.interpolation)), 307 | ), 308 | 309 | recipe_line_prefix: (_) => choice("@-", "-@", "@", "-"), 310 | 311 | // Any shebang. Needs a named field to apply injection queries correctly. 312 | shebang: ($) => 313 | seq(/#![ \t]*/, choice($._shebang_with_lang, $._opaque_shebang)), 314 | 315 | // Shebang with a nested `language` token that we can extract 316 | _shebang_with_lang: ($) => 317 | seq( 318 | /\S*\//, 319 | optional(seq("env", repeat(SHEBANG_ENV_FLAG))), 320 | alias($.identifier, $.language), 321 | /.*/, 322 | ), 323 | 324 | // Fallback shebang, any string 325 | _opaque_shebang: (_) => /[^/\n]+/, 326 | 327 | // string : STRING 328 | // | INDENTED_STRING 329 | // | RAW_STRING 330 | // | INDENTED_RAW_STRING 331 | string: ($) => 332 | choice( 333 | $._string_indented, 334 | $._raw_string_indented, 335 | $._string, 336 | // _raw_string, can't be written as a separate inline for osm reason 337 | /'[^']*'/, 338 | ), 339 | 340 | _raw_string_indented: (_) => seq("'''", repeat(/./), "'''"), 341 | _string: ($) => seq('"', repeat(choice($.escape_sequence, /[^\\"]+/)), '"'), 342 | // We need try two separate munches so neither escape sequences nor 343 | // potential closing quotes get eaten. 344 | _string_indented: ($) => 345 | seq('"""', repeat(choice($.escape_sequence, /[^\\]?[^\\"]+/)), '"""'), 346 | 347 | escape_sequence: (_) => ESCAPE_SEQUENCE, 348 | 349 | _backticked: ($) => seq("`", optional($.command_body), "`"), 350 | _indented_backticked: ($) => seq("```", optional($.command_body), "```"), 351 | 352 | command_body: ($) => repeat1(choice($.interpolation, /./)), 353 | 354 | // interpolation : '{{' expression '}}' 355 | interpolation: ($) => seq("{{", $.expression, "}}"), 356 | 357 | identifier: (_) => /[a-zA-Z_][a-zA-Z0-9_-]*/, 358 | 359 | // Numbers aren't allowed as values, but we capture them anyway as errors so 360 | // they don't mess up the whole syntax 361 | numeric_error: (_) => /(\d+\.\d*|\d+)/, 362 | 363 | // `# ...` comment 364 | comment: (_) => token(prec(-1, /#.*/)), 365 | }, 366 | }); 367 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | src := justfile_directory() / "src" 2 | bindings := justfile_directory() / "bindings" 3 | fuzzer := justfile_directory() / "fuzzer" 4 | target := justfile_directory() / "target" 5 | bin_dir := target / "bin" 6 | obj_dir := target / "obj" 7 | debug_out := bin_dir / "debug.out" 8 | fuzz_out := bin_dir / "fuzz.out" 9 | 10 | ts_path := justfile_directory() / "repositories" / "tree-sitter" 11 | ts_repo := "https://github.com/tree-sitter/tree-sitter" 12 | ts_branch := "release-0.24" # release tags aren't on `master` 13 | ts_sha := "fc8c1863e2e5724a0c40bb6e6cfc8631bfe5908b" # v0.24.4 14 | 15 | just_path := justfile_directory() / "repositories" / "just" 16 | just_repo := "https://github.com/casey/just.git" 17 | just_branch := "master" 18 | just_sha := "f5bdffda344daca6c791303e4bb2006ee5a0b144" # 1.35.0 19 | 20 | include_args := "-Isrc/ -I" + ts_path + "/lib/include -Inode_modules/nan" + if os() == "macos" { 21 | " -I" + `xcrun --sdk macosx --show-sdk-path` + "/usr/include/" 22 | } else { 23 | "" 24 | } 25 | general_cflags := "-Wall -Werror --pedantic -Wno-format-pedantic" 26 | 27 | fuzzer_flags := env("FUZZER_FLAGS", "-fsanitize=fuzzer,address,undefined") 28 | fuzz_time := env("FUZZ_TOTAL_TIME", "1200") 29 | 30 | # Source files needed to build a parser 31 | parser_sources := src + "/scanner.c " + src + "/parser.c " + ts_path + "/lib/src/lib.c" 32 | 33 | base_cache_key := sha256_file(src / "scanner.c") + sha256_file(src / "parser.c") + sha256(parser_sources) + sha256(include_args) + sha256(general_cflags) + sha256(fuzzer_flags) 34 | 35 | # `timeout` is not available on all platforms, but perl often is. This needs a 36 | # bash shell. 37 | make_timeout_fn := '''timeout () { perl -e 'alarm shift; exec @ARGV' "$@"; }''' 38 | 39 | # Files that should parse with errors but not crash 40 | errors_expected := ''' 41 | test/timeout-1aa6bf37e914715f4aa49e6cf693f7abf81aaf8e 42 | test/crash-4b0422bb457cd6b39d1f8549f6739830254718a0z-assertion 43 | ''' 44 | 45 | # Files used for testing that Just itself might not understand 46 | no_just_parsing := ''' 47 | test/readme.just 48 | test/highlight/invalid-syntax.just 49 | ''' 50 | 51 | # List all recipes 52 | default: 53 | @just --list 54 | 55 | # Verify that a tool is installed 56 | _check-installed +dep: 57 | #!/bin/sh 58 | set -eau 59 | 60 | check_installed() { 61 | printf "checking $1... " 62 | 63 | if command -v "$1"; then 64 | echo "tool $1 found!" 65 | else 66 | echo 67 | echo "tool $1 NOT found. This may be needed for some functionality" 68 | fi 69 | } 70 | 71 | for d in {{ dep }}; do 72 | check_installed $d 73 | done 74 | 75 | # Install needed packages and make sure tools are setup 76 | setup *npm-args: 77 | #!/bin/sh 78 | set -eau 79 | just _check-installed npm cargo clang clang-tidy clang-format 80 | 81 | if which npm > /dev/null; then 82 | npm install --include=dev {{ npm-args }} 83 | else 84 | echo "npm not found: skipping install" 85 | fi 86 | 87 | # Lint with more minimal dependencies that can be run during pre-commit 88 | _lint-min: _clone-repo-tree-sitter configure-compile-database 89 | npm run lint:check 90 | git ls-files '**.c' | \ 91 | grep -v 'parser\.c' | \ 92 | grep -v 'bindings/python' | \ 93 | xargs -IFNAME sh -c 'echo "\nchecking file FNAME" && clang-tidy FNAME' 94 | 95 | # Run the linter for JS, C, Cargo, and Python. Requires clang-tidy, clippy, and ruff. 96 | lint: _lint-min 97 | cargo clippy 98 | @just _check-installed ruff 99 | ruff check . 100 | 101 | _out-dirs: 102 | mkdir -p "{{ bin_dir }}" 103 | mkdir -p "{{ obj_dir }}" 104 | 105 | alias fmt := format 106 | 107 | # Autoformat code. Requires Cargo, clang-format, and black. 108 | format: configure-compile-database 109 | npm run format:write 110 | git ls-files '**.c' | grep -v 'parser\.c' | \ 111 | xargs -IFNAME sh -c \ 112 | 'echo "\nformatting 'FNAME'" && clang-format -i FNAME --verbose' 113 | cargo fmt 114 | black . 115 | 116 | # Check formatting without editing 117 | format-check: configure-compile-database 118 | npm run format:check 119 | git ls-files '**.c' | grep -v 'parser\.c' | \ 120 | xargs -IFNAME sh -c \ 121 | 'echo "\nchecking formatting for 'FNAME'" && clang-format FNAME | diff -up - FNAME' 122 | cargo fmt --check 123 | 124 | # Generate the parser 125 | gen *extra-args: 126 | npx tree-sitter generate {{ extra-args }} 127 | python3 build-flavored-queries.py 128 | 129 | # Run formatting only on generated files 130 | npx prettier --write src/ 131 | 132 | # Only clang-format if it is available 133 | which clang-format > /dev/null && \ 134 | clang-format -i src/parser.c || \ 135 | echo "skipping clang-format" 136 | 137 | build-wasm: gen 138 | npx tree-sitter build --wasm 139 | 140 | alias t := test 141 | 142 | # Run tests that are built into tree-sitter, as well as integration and Cargo tests 143 | test *ts-test-args: gen 144 | npx tree-sitter test {{ ts-test-args }} 145 | @just test-parse-highlight 146 | @just verify-no-error-tests 147 | 148 | echo '\nRunning Cargo tests' 149 | 150 | # FIXME: xfail Rust CI on Windows because we are getting STATUS_DLL_NOT_FOUND 151 | {{ if os_family() + env("CI", "1") == "windows1" { "echo skipping tests on Windows" } else { "cargo test" } }} 152 | 153 | # Verify that tree-sitter can parse and highlight all files in the repo. Requires a tree-sitter configuration. 154 | test-parse-highlight: _clone-repo-just 155 | #!/usr/bin/env python3 156 | import re 157 | import os 158 | import subprocess as sp 159 | from pathlib import Path 160 | 161 | # Windows doesn't seem to evaluate PATH unless `shell=True`. 162 | if os.name == "nt": 163 | shell = True 164 | else: 165 | shell = False 166 | 167 | justfile_directory = Path(r"{{ justfile_directory() }}") 168 | just_path = Path(r"{{ just_path }}") 169 | 170 | repo_files = sp.check_output( 171 | ["git", "ls-files", "*.just", "*justfile", "*timeout*", "*crash*"], 172 | encoding="utf8", shell=shell 173 | ) 174 | just_repo_files = sp.check_output( 175 | ["git", "-C", just_path, "ls-files", "*.just", "*justfile"], 176 | encoding="utf8", shell=shell 177 | ) 178 | 179 | files = set() 180 | error_files = set() 181 | skip_just = [] 182 | 183 | for line in repo_files.splitlines(): 184 | files.add(justfile_directory / line) 185 | 186 | for line in just_repo_files.splitlines(): 187 | files.add(just_path / line) 188 | 189 | for line in """{{ errors_expected }}""".splitlines(): 190 | line = re.sub("#.*", "", line).strip() 191 | if len(line) == 0: 192 | continue 193 | 194 | error_files.add(justfile_directory / line) 195 | 196 | for line in """{{ no_just_parsing }}""".splitlines(): 197 | line = re.sub("#.*", "", line).strip() 198 | if len(line) == 0: 199 | continue 200 | 201 | skip_just.append(justfile_directory / line) 202 | 203 | files -= error_files 204 | 205 | print("Checking parsing and highlighting...") 206 | 207 | ts_cmd = ["npx", "tree-sitter"] 208 | scope_args = ["--scope", "source.just"] 209 | 210 | for fname in files: 211 | print(f"Checking '{fname}': parse'") 212 | sp.check_output( 213 | ts_cmd + ["parse", fname] + scope_args, timeout=10, shell=shell 214 | ) 215 | 216 | print(f"Checking '{fname}': highlight'") 217 | sp.check_output( 218 | ts_cmd + ["highlight", fname] + scope_args, timeout=10, shell=shell 219 | ) 220 | 221 | # Verify that the `just` tool parses all files we are using 222 | if not fname in skip_just: 223 | sp.check_output( 224 | ["just", "--list", "--unstable", "--justfile", fname], 225 | timeout=10, shell=shell 226 | ) 227 | 228 | print("Checking parsing and highlighting for invalid files...") 229 | 230 | for fname in error_files: 231 | cmd = ts_cmd + ["parse", fname] + scope_args 232 | try: 233 | print(f"Checking invalid source '{fname}': parse'") 234 | res = sp.check_output(cmd, timeout=10, shell=shell) 235 | except sp.CalledProcessError as e: 236 | if e.returncode != 1: # error code 1 is a highlight failure 237 | print("command completed with non-1 exit status") 238 | raise e 239 | else: 240 | raise AssertionError(f"failure expected but not found: {cmd} -> {res}") 241 | 242 | # Highlighting should always succeed 243 | print(f"Checking invalid source '{fname}': highlight'") 244 | sp.check_output( 245 | ts_cmd + ["highlight", fname] + scope_args, timeout=10, shell=shell 246 | ) 247 | 248 | print( 249 | f"Successfully parsed {len(files) + len(error_files)} example files " 250 | f"with {len(error_files)} expected failures" 251 | ) 252 | 253 | # Make sure that no tests contain errors 254 | verify-no-error-tests: 255 | ! grep -nr -C4 -E '(ERROR|MISSING|UNEXPECTED)' test 256 | 257 | # Helper to rebuild helix grammars 258 | hx-build: 259 | hx --grammar build 260 | 261 | # Configure tree-sitter to use this directory 262 | configure-tree-sitter: 263 | #!/usr/bin/env python3 264 | import json 265 | import os 266 | import subprocess as sp 267 | 268 | # Windows doesn't seem to evaluate PATH unless `shell=True`. 269 | if os.name == "nt": 270 | shell = True 271 | else: 272 | shell = False 273 | 274 | cfg_fname = r"""{{ config_directory() / "tree-sitter" / "config.json" }}""" 275 | if not os.path.isfile(cfg_fname): 276 | sp.run(["npx", "tree-sitter", "init-config"], check=True, shell=shell) 277 | 278 | with open(cfg_fname, "r+") as f: 279 | j = json.load(f) 280 | f.seek(0) 281 | 282 | # Add the tree-sitter-just directory to the config file 283 | parent_dir = os.path.dirname(r"{{ justfile_directory() }}") 284 | j["parser-directories"].append(parent_dir) 285 | json.dump(j, f) 286 | 287 | f.truncate() 288 | 289 | # Run lint and check formatting 290 | ci-codestyle: lint format-check 291 | 292 | # Make sure that files have not changed 293 | ci-validate-generated-files exit-code="1": 294 | #!/bin/sh 295 | set -eaux 296 | 297 | git tag ci-tmp-pre-updates 298 | 299 | just gen 300 | 301 | failed=false 302 | git diff ci-tmp-pre-updates --text --exit-code || failed=true 303 | git tag -d ci-tmp-pre-updates 304 | 305 | if ! [ "$failed" = "false" ]; then 306 | echo '::warning::Generated files are out of date!' 307 | echo '::warning::run `just gen` and commit the changes' 308 | 309 | # We use an exit code so that we can use this as either a warning or error 310 | exit {{ exit-code }} 311 | fi 312 | 313 | # Run a subset of CI checks before committing. 314 | pre-commit: _lint-min format-check 315 | 316 | # Install pre-commit hooks 317 | pre-commit-install: 318 | #!/bin/sh 319 | fname="{{ justfile_directory() }}/.git/hooks/pre-commit" 320 | touch "$fname" 321 | chmod +x "$fname" 322 | 323 | cat << EOF > "$fname" 324 | #!/bin/sh 325 | just pre-commit 326 | EOF 327 | 328 | # Clone or update a repo 329 | _clone-repo url path sha branch: 330 | #!/bin/sh 331 | set -eaux 332 | 333 | if [ ! -d '{{ path }}' ]; then 334 | echo "Cloning {{ url }}" 335 | git clone '{{ url }}' '{{ path }}' --depth=1000 336 | fi 337 | 338 | actual_sha=$(git -C '{{ path }}' rev-parse HEAD) 339 | if [ "$actual_sha" != "{{ sha }}" ]; then 340 | echo "Updating {{ url }} to {{ sha }}" 341 | git -C '{{ path }}' fetch origin {{ branch }} 342 | git -C '{{ path }}' reset --hard '{{ sha }}' 343 | fi 344 | 345 | # Clone the tree-sitter repo 346 | _clone-repo-tree-sitter: (_clone-repo ts_repo ts_path ts_sha ts_branch) 347 | 348 | # Clone the just repo 349 | _clone-repo-just: (_clone-repo just_repo just_path just_sha just_branch) 350 | 351 | # Build a simple debug executable 352 | debug-build: _clone-repo-tree-sitter _out-dirs 353 | #!/bin/sh 354 | set -eau 355 | 356 | cache_key='{{ base_cache_key + sha256_file(bindings / "debug.c") }}' 357 | keyfile="{{ obj_dir }}/debug-build.cachekey" 358 | [ "$cache_key" = $(cat "$keyfile" 2> /dev/null || echo "") ] && exit 0 359 | 360 | clang -O0 -g -fsanitize=undefined ${CFLAGS:-} {{ include_args }} \ 361 | {{ parser_sources }} "{{ bindings }}/debug.c" \ 362 | -o {{ debug_out }} 363 | 364 | printf "$cache_key" > "$keyfile" 365 | 366 | # # Run the debug executable with one or more files 367 | debug-run *file-names: debug-build 368 | {{ debug_out }} {{file-names}} 369 | 370 | # Build and run the fuzzer 371 | fuzz *extra-args: (gen "--debug-build") _clone-repo-tree-sitter _out-dirs 372 | #!/bin/sh 373 | set -eaux 374 | 375 | "{{ fuzzer / "build-corpus.py" }}" 376 | 377 | artifacts="{{fuzzer}}/failures/" 378 | corpus="{{fuzzer}}/corpus" 379 | mkdir -p "$artifacts" 380 | opt_level="-O0" 381 | 382 | flags="{{ fuzzer_flags }}" 383 | flags="$flags -g $opt_level -std=gnu99" 384 | flags="$flags {{ include_args }}" 385 | 386 | sources="{{ parser_sources }} {{ fuzzer }}/entry.c" 387 | 388 | cache_key='{{ base_cache_key + sha256_file(fuzzer/ "entry.c") }}' 389 | keyfile="{{ obj_dir }}/fuzz.cachekey" 390 | [ "$cache_key" = $(cat "$keyfile" || echo "") ] || 391 | clang $flags -o "{{ fuzz_out }}" $sources 392 | 393 | printf "$cache_key" > "$keyfile" 394 | 395 | fuzzer_flags="-artifact_prefix=$artifacts -timeout=20 -max_total_time={{ fuzz_time }} -jobs={{ num_cpus() }}" 396 | 397 | echo "Starting fuzzing at $(date -u -Is)" 398 | LD_LIBRARY_PATH="{{ ts_path }}" "{{ fuzz_out }}" "$corpus" $fuzzer_flags {{ extra-args }} 399 | 400 | # Configure the database used by clang-format, clang-tidy, and language servers 401 | configure-compile-database: 402 | #!/usr/bin/env python3 403 | import json 404 | src = r"{{ src }}" 405 | include_args = r"{{ include_args }}" 406 | general_cflags = r"{{ general_cflags }}" 407 | 408 | sources = [ 409 | ("bindings/debug.c", r"{{ debug_out }}"), 410 | ("fuzzer/entry.c", r"{{ fuzz_out }}"), 411 | ("src/parser.c", r"{{ obj_dir / "parser.o" }}"), 412 | ("src/scanner.c", r"{{ obj_dir / "scanner.o" }}"), 413 | ] 414 | results = [] 415 | 416 | for (input, output) in sources: 417 | results.append({ 418 | "directory": f"{src}", 419 | "command": f"clang {include_args} {input} {general_cflags}", 420 | "file": f"{src}/{input}", 421 | "output": f"{src}/{output}", 422 | }) 423 | 424 | with open("compile_commands.json", "w") as f: 425 | json.dump(results, f, indent=4) 426 | -------------------------------------------------------------------------------- /lua/tree-sitter-just/init.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | local is_windows 4 | if jit ~= nil then 5 | is_windows = jit.os == "Windows" 6 | else 7 | is_windows = package.config:sub(1, 1) == "\\" 8 | end 9 | local get_separator = function() 10 | if is_windows then 11 | return "\\" 12 | end 13 | return "/" 14 | end 15 | local join_paths = function(...) 16 | local separator = get_separator() 17 | return table.concat({ ... }, separator) 18 | end 19 | 20 | function M.setup(arg) 21 | local parser_config = require("nvim-treesitter.parsers").get_parser_configs() 22 | parser_config.just = { 23 | install_info = { 24 | url = arg["local"] and join_paths( 25 | vim.fn.stdpath("data"), 26 | "site", 27 | "pack", 28 | "packer", 29 | "start", 30 | "tree-sitter-just" 31 | ) or "https://github.com/IndianBoy42/tree-sitter-just", -- local path or git repo 32 | files = { "src/parser.c", "src/scanner.c" }, 33 | branch = "main", 34 | }, 35 | maintainers = { "@IndianBoy42" }, 36 | } 37 | local ok, ft = pcall(require, "filetype") 38 | if ok then 39 | ft.setup({ 40 | overrides = { 41 | extensions = { 42 | just = "just", 43 | }, 44 | literals = { 45 | Justfile = "just", 46 | justfile = "just", 47 | [".Justfile"] = "just", 48 | [".justfile"] = "just", 49 | }, 50 | }, 51 | }) 52 | end 53 | end 54 | 55 | return M 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tree-sitter-just", 3 | "author": "Anshuman Medhi ", 4 | "contributors": [ 5 | "Trevor Gross ", 6 | "Amaan Qureshi " 7 | ], 8 | "license": "MIT", 9 | "version": "0.1.0", 10 | "description": "Justfiles grammar for tree-sitter", 11 | "main": "bindings/node", 12 | "types": "bindings/node", 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/IndianBoy42/tree-sitter-just" 16 | }, 17 | "keywords": [ 18 | "tree-sitter", 19 | "justfiles", 20 | "parser", 21 | "lexer" 22 | ], 23 | "files": [ 24 | "grammar.js", 25 | "binding.gyp", 26 | "prebuilds/**", 27 | "bindings/node/*", 28 | "queries/*", 29 | "src/**" 30 | ], 31 | "dependencies": { 32 | "tree-sitter": "^0.22.1", 33 | "prettier": "^3.3.3", 34 | "node-addon-api": "^8.2.2", 35 | "node-gyp-build": "^4.8.3" 36 | }, 37 | "peerDependencies": { 38 | "tree-sitter": "^0.22.1" 39 | }, 40 | "peerDependenciesMeta": { 41 | "tree_sitter": { 42 | "optional": true 43 | } 44 | }, 45 | "devDependencies": { 46 | "eslint": ">=9.15.0", 47 | "eslint-config-google": "^0.14.0", 48 | "eslint-config-prettier": "^9.1.0", 49 | "tree-sitter-cli": "^0.24.4", 50 | "prebuildify": "^6.0.1" 51 | }, 52 | "scripts": { 53 | "format:check": "prettier --check .", 54 | "format:write": "prettier --write .", 55 | "lint:check": "eslint .", 56 | "lint:fix": "eslint --fix .", 57 | "install": "node-gyp-build", 58 | "prebuildify": "prebuildify --napi --strip" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=42", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "tree-sitter-just" 7 | description = "Just grammar for tree-sitter" 8 | version = "0.0.1" 9 | keywords = ["incremental", "parsing", "tree-sitter", "just"] 10 | classifiers = [ 11 | "Intended Audience :: Developers", 12 | "License :: OSI Approved :: MIT License", 13 | "Topic :: Software Development :: Compilers", 14 | "Topic :: Text Processing :: Linguistic", 15 | "Typing :: Typed" 16 | ] 17 | requires-python = ">=3.8" 18 | license.text = "MIT" 19 | readme = "README.md" 20 | 21 | [project.urls] 22 | Homepage = "https://github.com/tree-sitter/tree-sitter-just" 23 | 24 | [project.optional-dependencies] 25 | core = ["tree-sitter~=0.21"] 26 | 27 | [tool.cibuildwheel] 28 | build = "cp38-*" 29 | build-frontend = "build" 30 | -------------------------------------------------------------------------------- /queries-flavored/helix/folds.scm: -------------------------------------------------------------------------------- 1 | ; File autogenerated by build-queries-nvim.py; do not edit 2 | 3 | ; Define collapse points 4 | 5 | ([ 6 | (recipe) 7 | (string) 8 | (external_command) 9 | ] @fold 10 | (#trim! @fold)) 11 | -------------------------------------------------------------------------------- /queries-flavored/helix/highlights.scm: -------------------------------------------------------------------------------- 1 | ; File autogenerated by build-queries-nvim.py; do not edit 2 | 3 | ; This file specifies how matched syntax patterns should be highlighted 4 | 5 | [ 6 | "export" 7 | "import" 8 | ] @keyword.control.import 9 | 10 | "mod" @keyword.directive 11 | 12 | [ 13 | "alias" 14 | "set" 15 | "shell" 16 | ] @keyword 17 | 18 | [ 19 | "if" 20 | "else" 21 | ] @keyword.control.conditional 22 | 23 | ; Variables 24 | 25 | (value 26 | (identifier) @variable) 27 | 28 | (alias 29 | left: (identifier) @variable) 30 | 31 | (assignment 32 | left: (identifier) @variable) 33 | 34 | ; Functions 35 | 36 | (recipe_header 37 | name: (identifier) @function) 38 | 39 | (dependency 40 | name: (identifier) @function) 41 | 42 | (dependency_expression 43 | name: (identifier) @function) 44 | 45 | (function_call 46 | name: (identifier) @function) 47 | 48 | ; Parameters 49 | 50 | (parameter 51 | name: (identifier) @variable.parameter) 52 | 53 | ; Namespaces 54 | 55 | (module 56 | name: (identifier) @namespace) 57 | 58 | ; Operators 59 | 60 | [ 61 | ":=" 62 | "?" 63 | "==" 64 | "!=" 65 | "=~" 66 | "@" 67 | "=" 68 | "$" 69 | "*" 70 | "+" 71 | "&&" 72 | "@-" 73 | "-@" 74 | "-" 75 | "/" 76 | ":" 77 | ] @operator 78 | 79 | ; Punctuation 80 | 81 | "," @punctuation.delimiter 82 | 83 | [ 84 | "{" 85 | "}" 86 | "[" 87 | "]" 88 | "(" 89 | ")" 90 | "{{" 91 | "}}" 92 | ] @punctuation.bracket 93 | 94 | [ "`" "```" ] @punctuation.special 95 | 96 | ; Literals 97 | 98 | (boolean) @constant.builtin.boolean 99 | 100 | [ 101 | (string) 102 | (external_command) 103 | ] @string 104 | 105 | (escape_sequence) @constant.character.escape 106 | 107 | ; Comments 108 | 109 | (comment) @comment.line 110 | 111 | (shebang) @keyword.directive 112 | 113 | ; highlight known settings (filtering does not always work) 114 | (setting 115 | left: (identifier) @keyword 116 | (#any-of? @keyword 117 | "allow-duplicate-recipes" 118 | "allow-duplicate-variables" 119 | "dotenv-filename" 120 | "dotenv-load" 121 | "dotenv-path" 122 | "dotenv-required" 123 | "export" 124 | "fallback" 125 | "ignore-comments" 126 | "positional-arguments" 127 | "shell" 128 | "shell-interpreter" 129 | "tempdir" 130 | "windows-powershell" 131 | "windows-shell" 132 | "working-directory")) 133 | 134 | ; highlight known attributes (filtering does not always work) 135 | (attribute 136 | (identifier) @attribute 137 | (#any-of? @attribute 138 | "confirm" 139 | "doc" 140 | "extension" 141 | "group" 142 | "linux" 143 | "macos" 144 | "no-cd" 145 | "no-exit-message" 146 | "no-quiet" 147 | "positional-arguments" 148 | "private" 149 | "script" 150 | "unix" 151 | "windows")) 152 | 153 | ; Numbers are part of the syntax tree, even if disallowed 154 | (numeric_error) @error 155 | -------------------------------------------------------------------------------- /queries-flavored/helix/indents.scm: -------------------------------------------------------------------------------- 1 | ; File autogenerated by build-queries-nvim.py; do not edit 2 | 3 | ; This query specifies how to auto-indent logical blocks. 4 | ; 5 | ; Better documentation with diagrams is in https://docs.helix-editor.com/guides/indent.html 6 | 7 | [ 8 | (recipe) 9 | (string) 10 | (external_command) 11 | ] @indent @extend 12 | -------------------------------------------------------------------------------- /queries-flavored/helix/injections.scm: -------------------------------------------------------------------------------- 1 | ; File autogenerated by build-queries-nvim.py; do not edit 2 | 3 | ; Specify nested languages that live within a `justfile` 4 | 5 | ; FIXME: these are not compatible with helix due to precedence 6 | 7 | ; ================ Always applicable ================ 8 | 9 | ((comment) @injection.content 10 | (#set! injection.language "comment")) 11 | 12 | ; Highlight the RHS of `=~` as regex 13 | ((regex_literal 14 | (_) @injection.content) 15 | (#set! injection.language "regex")) 16 | 17 | ; ================ Global defaults ================ 18 | 19 | ; Default everything to be bash 20 | (recipe_body 21 | !shebang 22 | (#set! injection.language "bash") 23 | (#set! injection.include-children)) @injection.content 24 | 25 | (external_command 26 | (command_body) @injection.content 27 | (#set! injection.language "bash")) 28 | 29 | ; ================ Global language specified ================ 30 | ; Global language is set with something like one of the following: 31 | ; 32 | ; set shell := ["bash", "-c", ...] 33 | ; set shell := ["pwsh.exe"] 34 | ; 35 | ; We can extract the first item of the array, but we can't extract the language 36 | ; name from the string with something like regex. So instead we special case 37 | ; two things: powershell, which is likely to come with a `.exe` attachment that 38 | ; we need to strip, and everything else which hopefully has no extension. We 39 | ; separate this with a `#match?`. 40 | ; 41 | ; Unfortunately, there also isn't a way to allow arbitrary nesting or 42 | ; alternatively set "global" capture variables. So we can set this for item- 43 | ; level external commands, but not for e.g. external commands within an 44 | ; expression without getting _really_ annoying. Should at least look fine since 45 | ; they default to bash. Limitations... 46 | ; See https://github.com/tree-sitter/tree-sitter/issues/880 for more on that. 47 | 48 | (source_file 49 | (setting "shell" ":=" "[" (string) @_langstr 50 | (#match? @_langstr ".*(powershell|pwsh|cmd).*") 51 | (#set! injection.language "powershell")) 52 | [ 53 | (recipe 54 | (recipe_body 55 | !shebang 56 | (#set! injection.include-children)) @injection.content) 57 | 58 | (assignment 59 | (expression 60 | (value 61 | (external_command 62 | (command_body) @injection.content)))) 63 | ]) 64 | 65 | (source_file 66 | (setting "shell" ":=" "[" (string) @injection.language 67 | (#not-match? @injection.language ".*(powershell|pwsh|cmd).*")) 68 | [ 69 | (recipe 70 | (recipe_body 71 | !shebang 72 | (#set! injection.include-children)) @injection.content) 73 | 74 | (assignment 75 | (expression 76 | (value 77 | (external_command 78 | (command_body) @injection.content)))) 79 | ]) 80 | 81 | ; ================ Recipe language specified - Helix only ================ 82 | 83 | ; Set highlighting for recipes that specify a language using builtin shebang matching 84 | (recipe_body 85 | (shebang) @injection.shebang 86 | (#set! injection.include-children)) @injection.content 87 | -------------------------------------------------------------------------------- /queries-flavored/helix/locals.scm: -------------------------------------------------------------------------------- 1 | ; File autogenerated by build-queries-nvim.py; do not edit 2 | 3 | ; This file tells us about the scope of variables so e.g. local 4 | ; variables override global functions with the same name 5 | 6 | ; Scope 7 | 8 | (recipe) @local.scope 9 | 10 | ; Definitions 11 | 12 | (alias 13 | left: (identifier) @local.definition) 14 | 15 | (assignment 16 | left: (identifier) @local.definition) 17 | 18 | (module 19 | name: (identifier) @local.definition) 20 | 21 | (parameter 22 | name: (identifier) @local.definition) 23 | 24 | (recipe_header 25 | name: (identifier) @local.definition) 26 | 27 | ; References 28 | 29 | (alias 30 | right: (identifier) @local.reference) 31 | 32 | (function_call 33 | name: (identifier) @local.reference) 34 | 35 | (dependency 36 | name: (identifier) @local.reference) 37 | 38 | (dependency_expression 39 | name: (identifier) @local.reference) 40 | 41 | (value 42 | (identifier) @local.reference) 43 | -------------------------------------------------------------------------------- /queries-flavored/helix/textobjects.scm: -------------------------------------------------------------------------------- 1 | ; File autogenerated by build-queries-nvim.py; do not edit 2 | 3 | ; Specify how to navigate around logical blocks in code 4 | 5 | (recipe 6 | (recipe_body) @function.inside) @function.around 7 | 8 | (parameters 9 | ((_) @parameter.inside . ","? @parameter.around)) @parameter.around 10 | 11 | (dependency_expression 12 | (_) @parameter.inside) @parameter.around 13 | 14 | (function_call 15 | arguments: (sequence 16 | (expression) @parameter.inside) @parameter.around) @function.around 17 | 18 | (comment) @comment.around 19 | -------------------------------------------------------------------------------- /queries-flavored/lapce/folds.scm: -------------------------------------------------------------------------------- 1 | ; File autogenerated by build-queries-nvim.py; do not edit 2 | 3 | ; Define collapse points 4 | 5 | ([ 6 | (recipe) 7 | (string) 8 | (external_command) 9 | ] @fold 10 | (#trim! @fold)) 11 | -------------------------------------------------------------------------------- /queries-flavored/lapce/highlights.scm: -------------------------------------------------------------------------------- 1 | ; File autogenerated by build-queries-nvim.py; do not edit 2 | 3 | ; This file specifies how matched syntax patterns should be highlighted 4 | 5 | [ 6 | "export" 7 | "import" 8 | ] @keyword.control.import 9 | 10 | "mod" @keyword.directive 11 | 12 | [ 13 | "alias" 14 | "set" 15 | "shell" 16 | ] @keyword 17 | 18 | [ 19 | "if" 20 | "else" 21 | ] @keyword.control.conditional 22 | 23 | ; Variables 24 | 25 | (value 26 | (identifier) @variable) 27 | 28 | (alias 29 | left: (identifier) @variable) 30 | 31 | (assignment 32 | left: (identifier) @variable) 33 | 34 | ; Functions 35 | 36 | (recipe_header 37 | name: (identifier) @function) 38 | 39 | (dependency 40 | name: (identifier) @function) 41 | 42 | (dependency_expression 43 | name: (identifier) @function) 44 | 45 | (function_call 46 | name: (identifier) @function) 47 | 48 | ; Parameters 49 | 50 | (parameter 51 | name: (identifier) @variable.parameter) 52 | 53 | ; Namespaces 54 | 55 | (module 56 | name: (identifier) @namespace) 57 | 58 | ; Operators 59 | 60 | [ 61 | ":=" 62 | "?" 63 | "==" 64 | "!=" 65 | "=~" 66 | "@" 67 | "=" 68 | "$" 69 | "*" 70 | "+" 71 | "&&" 72 | "@-" 73 | "-@" 74 | "-" 75 | "/" 76 | ":" 77 | ] @operator 78 | 79 | ; Punctuation 80 | 81 | "," @punctuation.delimiter 82 | 83 | [ 84 | "{" 85 | "}" 86 | "[" 87 | "]" 88 | "(" 89 | ")" 90 | "{{" 91 | "}}" 92 | ] @punctuation.bracket 93 | 94 | [ "`" "```" ] @punctuation.special 95 | 96 | ; Literals 97 | 98 | (boolean) @constant.builtin.boolean 99 | 100 | [ 101 | (string) 102 | (external_command) 103 | ] @string 104 | 105 | (escape_sequence) @constant.character.escape 106 | 107 | ; Comments 108 | 109 | (comment) @comment.line 110 | 111 | (shebang) @keyword.directive 112 | 113 | ; highlight known settings (filtering does not always work) 114 | (setting 115 | left: (identifier) @keyword 116 | (#any-of? @keyword 117 | "allow-duplicate-recipes" 118 | "allow-duplicate-variables" 119 | "dotenv-filename" 120 | "dotenv-load" 121 | "dotenv-path" 122 | "dotenv-required" 123 | "export" 124 | "fallback" 125 | "ignore-comments" 126 | "positional-arguments" 127 | "shell" 128 | "shell-interpreter" 129 | "tempdir" 130 | "windows-powershell" 131 | "windows-shell" 132 | "working-directory")) 133 | 134 | ; highlight known attributes (filtering does not always work) 135 | (attribute 136 | (identifier) @attribute 137 | (#any-of? @attribute 138 | "confirm" 139 | "doc" 140 | "extension" 141 | "group" 142 | "linux" 143 | "macos" 144 | "no-cd" 145 | "no-exit-message" 146 | "no-quiet" 147 | "positional-arguments" 148 | "private" 149 | "script" 150 | "unix" 151 | "windows")) 152 | 153 | ; Numbers are part of the syntax tree, even if disallowed 154 | (numeric_error) @error 155 | -------------------------------------------------------------------------------- /queries-flavored/lapce/indents.scm: -------------------------------------------------------------------------------- 1 | ; File autogenerated by build-queries-nvim.py; do not edit 2 | 3 | ; This query specifies how to auto-indent logical blocks. 4 | ; 5 | ; Better documentation with diagrams is in https://docs.helix-editor.com/guides/indent.html 6 | 7 | [ 8 | (recipe) 9 | (string) 10 | (external_command) 11 | ] @indent @extend 12 | -------------------------------------------------------------------------------- /queries-flavored/lapce/injections.scm: -------------------------------------------------------------------------------- 1 | ; File autogenerated by build-queries-nvim.py; do not edit 2 | 3 | ; Specify nested languages that live within a `justfile` 4 | 5 | ; FIXME: these are not compatible with helix due to precedence 6 | 7 | ; ================ Always applicable ================ 8 | 9 | ((comment) @injection.content 10 | (#set! injection.language "comment")) 11 | 12 | ; Highlight the RHS of `=~` as regex 13 | ((regex_literal 14 | (_) @injection.content) 15 | (#set! injection.language "regex")) 16 | 17 | ; ================ Global defaults ================ 18 | 19 | ; Default everything to be bash 20 | (recipe_body 21 | !shebang 22 | (#set! injection.language "bash") 23 | (#set! injection.include-children)) @injection.content 24 | 25 | (external_command 26 | (command_body) @injection.content 27 | (#set! injection.language "bash")) 28 | 29 | ; ================ Global language specified ================ 30 | ; Global language is set with something like one of the following: 31 | ; 32 | ; set shell := ["bash", "-c", ...] 33 | ; set shell := ["pwsh.exe"] 34 | ; 35 | ; We can extract the first item of the array, but we can't extract the language 36 | ; name from the string with something like regex. So instead we special case 37 | ; two things: powershell, which is likely to come with a `.exe` attachment that 38 | ; we need to strip, and everything else which hopefully has no extension. We 39 | ; separate this with a `#match?`. 40 | ; 41 | ; Unfortunately, there also isn't a way to allow arbitrary nesting or 42 | ; alternatively set "global" capture variables. So we can set this for item- 43 | ; level external commands, but not for e.g. external commands within an 44 | ; expression without getting _really_ annoying. Should at least look fine since 45 | ; they default to bash. Limitations... 46 | ; See https://github.com/tree-sitter/tree-sitter/issues/880 for more on that. 47 | 48 | (source_file 49 | (setting "shell" ":=" "[" (string) @_langstr 50 | (#match? @_langstr ".*(powershell|pwsh|cmd).*") 51 | (#set! injection.language "powershell")) 52 | [ 53 | (recipe 54 | (recipe_body 55 | !shebang 56 | (#set! injection.include-children)) @injection.content) 57 | 58 | (assignment 59 | (expression 60 | (value 61 | (external_command 62 | (command_body) @injection.content)))) 63 | ]) 64 | 65 | (source_file 66 | (setting "shell" ":=" "[" (string) @injection.language 67 | (#not-match? @injection.language ".*(powershell|pwsh|cmd).*")) 68 | [ 69 | (recipe 70 | (recipe_body 71 | !shebang 72 | (#set! injection.include-children)) @injection.content) 73 | 74 | (assignment 75 | (expression 76 | (value 77 | (external_command 78 | (command_body) @injection.content)))) 79 | ]) 80 | 81 | ; ================ Recipe language specified ================ 82 | 83 | ; Set highlighting for recipes that specify a language, using the exact name by default 84 | (recipe_body ; 85 | (shebang ; 86 | (language) @injection.language) 87 | (#not-any-of? @injection.language "python3" "nodejs" "node" "uv") 88 | (#set! injection.include-children)) @injection.content 89 | 90 | ; Transform some known executables 91 | 92 | ; python3/uv -> python 93 | (recipe_body 94 | (shebang 95 | (language) @_lang) 96 | (#any-of? @_lang "python3" "uv") 97 | (#set! injection.language "python") 98 | (#set! injection.include-children)) @injection.content 99 | 100 | ; node/nodejs -> javascript 101 | (recipe_body 102 | (shebang 103 | (language) @_lang) 104 | (#any-of? @_lang "node" "nodejs") 105 | (#set! injection.language "javascript") 106 | (#set! injection.include-children)) @injection.content 107 | -------------------------------------------------------------------------------- /queries-flavored/lapce/locals.scm: -------------------------------------------------------------------------------- 1 | ; File autogenerated by build-queries-nvim.py; do not edit 2 | 3 | ; This file tells us about the scope of variables so e.g. local 4 | ; variables override global functions with the same name 5 | 6 | ; Scope 7 | 8 | (recipe) @local.scope 9 | 10 | ; Definitions 11 | 12 | (alias 13 | left: (identifier) @local.definition) 14 | 15 | (assignment 16 | left: (identifier) @local.definition) 17 | 18 | (module 19 | name: (identifier) @local.definition) 20 | 21 | (parameter 22 | name: (identifier) @local.definition) 23 | 24 | (recipe_header 25 | name: (identifier) @local.definition) 26 | 27 | ; References 28 | 29 | (alias 30 | right: (identifier) @local.reference) 31 | 32 | (function_call 33 | name: (identifier) @local.reference) 34 | 35 | (dependency 36 | name: (identifier) @local.reference) 37 | 38 | (dependency_expression 39 | name: (identifier) @local.reference) 40 | 41 | (value 42 | (identifier) @local.reference) 43 | -------------------------------------------------------------------------------- /queries-flavored/lapce/textobjects.scm: -------------------------------------------------------------------------------- 1 | ; File autogenerated by build-queries-nvim.py; do not edit 2 | 3 | ; Specify how to navigate around logical blocks in code 4 | 5 | (recipe 6 | (recipe_body) @function.inside) @function.around 7 | 8 | (parameters 9 | ((_) @parameter.inside . ","? @parameter.around)) @parameter.around 10 | 11 | (dependency_expression 12 | (_) @parameter.inside) @parameter.around 13 | 14 | (function_call 15 | arguments: (sequence 16 | (expression) @parameter.inside) @parameter.around) @function.around 17 | 18 | (comment) @comment.around 19 | -------------------------------------------------------------------------------- /queries-flavored/zed/folds.scm: -------------------------------------------------------------------------------- 1 | ; File autogenerated by build-queries-nvim.py; do not edit 2 | 3 | ; Define collapse points 4 | 5 | ([ 6 | (recipe) 7 | (string) 8 | (external_command) 9 | ] @fold 10 | (#trim! @fold)) 11 | -------------------------------------------------------------------------------- /queries-flavored/zed/highlights.scm: -------------------------------------------------------------------------------- 1 | ; File autogenerated by build-queries-nvim.py; do not edit 2 | 3 | ; This file specifies how matched syntax patterns should be highlighted 4 | 5 | [ 6 | "export" 7 | "import" 8 | ] @keyword.control.import 9 | 10 | "mod" @keyword.directive 11 | 12 | [ 13 | "alias" 14 | "set" 15 | "shell" 16 | ] @keyword 17 | 18 | [ 19 | "if" 20 | "else" 21 | ] @keyword.control.conditional 22 | 23 | ; Variables 24 | 25 | (value 26 | (identifier) @variable) 27 | 28 | (alias 29 | left: (identifier) @variable) 30 | 31 | (assignment 32 | left: (identifier) @variable) 33 | 34 | ; Functions 35 | 36 | (recipe_header 37 | name: (identifier) @function) 38 | 39 | (dependency 40 | name: (identifier) @function) 41 | 42 | (dependency_expression 43 | name: (identifier) @function) 44 | 45 | (function_call 46 | name: (identifier) @function) 47 | 48 | ; Parameters 49 | 50 | (parameter 51 | name: (identifier) @variable.parameter) 52 | 53 | ; Namespaces 54 | 55 | (module 56 | name: (identifier) @namespace) 57 | 58 | ; Operators 59 | 60 | [ 61 | ":=" 62 | "?" 63 | "==" 64 | "!=" 65 | "=~" 66 | "@" 67 | "=" 68 | "$" 69 | "*" 70 | "+" 71 | "&&" 72 | "@-" 73 | "-@" 74 | "-" 75 | "/" 76 | ":" 77 | ] @operator 78 | 79 | ; Punctuation 80 | 81 | "," @punctuation.delimiter 82 | 83 | [ 84 | "{" 85 | "}" 86 | "[" 87 | "]" 88 | "(" 89 | ")" 90 | "{{" 91 | "}}" 92 | ] @punctuation.bracket 93 | 94 | [ "`" "```" ] @punctuation.special 95 | 96 | ; Literals 97 | 98 | (boolean) @constant.builtin.boolean 99 | 100 | [ 101 | (string) 102 | (external_command) 103 | ] @string 104 | 105 | (escape_sequence) @constant.character.escape 106 | 107 | ; Comments 108 | 109 | (comment) @comment.line 110 | 111 | (shebang) @keyword.directive 112 | 113 | ; highlight known settings (filtering does not always work) 114 | (setting 115 | left: (identifier) @keyword 116 | (#any-of? @keyword 117 | "allow-duplicate-recipes" 118 | "allow-duplicate-variables" 119 | "dotenv-filename" 120 | "dotenv-load" 121 | "dotenv-path" 122 | "dotenv-required" 123 | "export" 124 | "fallback" 125 | "ignore-comments" 126 | "positional-arguments" 127 | "shell" 128 | "shell-interpreter" 129 | "tempdir" 130 | "windows-powershell" 131 | "windows-shell" 132 | "working-directory")) 133 | 134 | ; highlight known attributes (filtering does not always work) 135 | (attribute 136 | (identifier) @attribute 137 | (#any-of? @attribute 138 | "confirm" 139 | "doc" 140 | "extension" 141 | "group" 142 | "linux" 143 | "macos" 144 | "no-cd" 145 | "no-exit-message" 146 | "no-quiet" 147 | "positional-arguments" 148 | "private" 149 | "script" 150 | "unix" 151 | "windows")) 152 | 153 | ; Numbers are part of the syntax tree, even if disallowed 154 | (numeric_error) @error 155 | -------------------------------------------------------------------------------- /queries-flavored/zed/indents.scm: -------------------------------------------------------------------------------- 1 | ; File autogenerated by build-queries-nvim.py; do not edit 2 | 3 | ; This query specifies how to auto-indent logical blocks. 4 | ; 5 | ; Better documentation with diagrams is in https://docs.helix-editor.com/guides/indent.html 6 | 7 | [ 8 | (recipe) 9 | (string) 10 | (external_command) 11 | ] @indent @extend 12 | -------------------------------------------------------------------------------- /queries-flavored/zed/injections.scm: -------------------------------------------------------------------------------- 1 | ; File autogenerated by build-queries-nvim.py; do not edit 2 | 3 | ; Specify nested languages that live within a `justfile` 4 | 5 | ; FIXME: these are not compatible with helix due to precedence 6 | 7 | ; ================ Always applicable ================ 8 | 9 | ((comment) @injection.content 10 | (#set! injection.language "comment")) 11 | 12 | ; Highlight the RHS of `=~` as regex 13 | ((regex_literal 14 | (_) @injection.content) 15 | (#set! injection.language "regex")) 16 | 17 | ; ================ Global defaults ================ 18 | 19 | ; Default everything to be bash 20 | (recipe_body 21 | !shebang 22 | (#set! injection.language "bash") 23 | (#set! injection.include-children)) @injection.content 24 | 25 | (external_command 26 | (command_body) @injection.content 27 | (#set! injection.language "bash")) 28 | 29 | ; ================ Global language specified ================ 30 | ; Global language is set with something like one of the following: 31 | ; 32 | ; set shell := ["bash", "-c", ...] 33 | ; set shell := ["pwsh.exe"] 34 | ; 35 | ; We can extract the first item of the array, but we can't extract the language 36 | ; name from the string with something like regex. So instead we special case 37 | ; two things: powershell, which is likely to come with a `.exe` attachment that 38 | ; we need to strip, and everything else which hopefully has no extension. We 39 | ; separate this with a `#match?`. 40 | ; 41 | ; Unfortunately, there also isn't a way to allow arbitrary nesting or 42 | ; alternatively set "global" capture variables. So we can set this for item- 43 | ; level external commands, but not for e.g. external commands within an 44 | ; expression without getting _really_ annoying. Should at least look fine since 45 | ; they default to bash. Limitations... 46 | ; See https://github.com/tree-sitter/tree-sitter/issues/880 for more on that. 47 | 48 | (source_file 49 | (setting "shell" ":=" "[" (string) @_langstr 50 | (#match? @_langstr ".*(powershell|pwsh|cmd).*") 51 | (#set! injection.language "powershell")) 52 | [ 53 | (recipe 54 | (recipe_body 55 | !shebang 56 | (#set! injection.include-children)) @injection.content) 57 | 58 | (assignment 59 | (expression 60 | (value 61 | (external_command 62 | (command_body) @injection.content)))) 63 | ]) 64 | 65 | (source_file 66 | (setting "shell" ":=" "[" (string) @injection.language 67 | (#not-match? @injection.language ".*(powershell|pwsh|cmd).*")) 68 | [ 69 | (recipe 70 | (recipe_body 71 | !shebang 72 | (#set! injection.include-children)) @injection.content) 73 | 74 | (assignment 75 | (expression 76 | (value 77 | (external_command 78 | (command_body) @injection.content)))) 79 | ]) 80 | 81 | ; ================ Recipe language specified ================ 82 | 83 | ; Set highlighting for recipes that specify a language, using the exact name by default 84 | (recipe_body ; 85 | (shebang ; 86 | (language) @injection.language) 87 | (#not-any-of? @injection.language "python3" "nodejs" "node" "uv") 88 | (#set! injection.include-children)) @injection.content 89 | 90 | ; Transform some known executables 91 | 92 | ; python3/uv -> python 93 | (recipe_body 94 | (shebang 95 | (language) @_lang) 96 | (#any-of? @_lang "python3" "uv") 97 | (#set! injection.language "python") 98 | (#set! injection.include-children)) @injection.content 99 | 100 | ; node/nodejs -> javascript 101 | (recipe_body 102 | (shebang 103 | (language) @_lang) 104 | (#any-of? @_lang "node" "nodejs") 105 | (#set! injection.language "javascript") 106 | (#set! injection.include-children)) @injection.content 107 | -------------------------------------------------------------------------------- /queries-flavored/zed/locals.scm: -------------------------------------------------------------------------------- 1 | ; File autogenerated by build-queries-nvim.py; do not edit 2 | 3 | ; This file tells us about the scope of variables so e.g. local 4 | ; variables override global functions with the same name 5 | 6 | ; Scope 7 | 8 | (recipe) @local.scope 9 | 10 | ; Definitions 11 | 12 | (alias 13 | left: (identifier) @local.definition) 14 | 15 | (assignment 16 | left: (identifier) @local.definition) 17 | 18 | (module 19 | name: (identifier) @local.definition) 20 | 21 | (parameter 22 | name: (identifier) @local.definition) 23 | 24 | (recipe_header 25 | name: (identifier) @local.definition) 26 | 27 | ; References 28 | 29 | (alias 30 | right: (identifier) @local.reference) 31 | 32 | (function_call 33 | name: (identifier) @local.reference) 34 | 35 | (dependency 36 | name: (identifier) @local.reference) 37 | 38 | (dependency_expression 39 | name: (identifier) @local.reference) 40 | 41 | (value 42 | (identifier) @local.reference) 43 | -------------------------------------------------------------------------------- /queries-flavored/zed/textobjects.scm: -------------------------------------------------------------------------------- 1 | ; File autogenerated by build-queries-nvim.py; do not edit 2 | 3 | ; Specify how to navigate around logical blocks in code 4 | 5 | (recipe 6 | (recipe_body) @function.inside) @function.around 7 | 8 | (parameters 9 | ((_) @parameter.inside . ","? @parameter.around)) @parameter.around 10 | 11 | (dependency_expression 12 | (_) @parameter.inside) @parameter.around 13 | 14 | (function_call 15 | arguments: (sequence 16 | (expression) @parameter.inside) @parameter.around) @function.around 17 | 18 | (comment) @comment.around 19 | -------------------------------------------------------------------------------- /queries-src/folds.scm: -------------------------------------------------------------------------------- 1 | ; Define collapse points 2 | 3 | ([ 4 | (recipe) 5 | (string) 6 | (external_command) 7 | ] @fold 8 | (#trim! @fold)) 9 | -------------------------------------------------------------------------------- /queries-src/highlights.scm: -------------------------------------------------------------------------------- 1 | ; This file specifies how matched syntax patterns should be highlighted 2 | 3 | [ 4 | "export" 5 | "import" 6 | ] @keyword.control.import 7 | 8 | "mod" @keyword.module 9 | 10 | [ 11 | "alias" 12 | "set" 13 | "shell" 14 | ] @keyword 15 | 16 | [ 17 | "if" 18 | "else" 19 | ] @keyword.control.conditional 20 | 21 | ; Variables 22 | 23 | (value 24 | (identifier) @variable) 25 | 26 | (alias 27 | left: (identifier) @variable) 28 | 29 | (assignment 30 | left: (identifier) @variable) 31 | 32 | ; Functions 33 | 34 | (recipe_header 35 | name: (identifier) @function) 36 | 37 | (dependency 38 | name: (identifier) @function.call) 39 | 40 | (dependency_expression 41 | name: (identifier) @function.call) 42 | 43 | (function_call 44 | name: (identifier) @function.call) 45 | 46 | ; Parameters 47 | 48 | (parameter 49 | name: (identifier) @variable.parameter) 50 | 51 | ; Namespaces 52 | 53 | (module 54 | name: (identifier) @namespace) 55 | 56 | ; Operators 57 | 58 | [ 59 | ":=" 60 | "?" 61 | "==" 62 | "!=" 63 | "=~" 64 | "@" 65 | "=" 66 | "$" 67 | "*" 68 | "+" 69 | "&&" 70 | "@-" 71 | "-@" 72 | "-" 73 | "/" 74 | ":" 75 | ] @operator 76 | 77 | ; Punctuation 78 | 79 | "," @punctuation.delimiter 80 | 81 | [ 82 | "{" 83 | "}" 84 | "[" 85 | "]" 86 | "(" 87 | ")" 88 | "{{" 89 | "}}" 90 | ] @punctuation.bracket 91 | 92 | [ "`" "```" ] @punctuation.special 93 | 94 | ; Literals 95 | 96 | (boolean) @constant.builtin.boolean 97 | 98 | [ 99 | (string) 100 | (external_command) 101 | ] @string 102 | 103 | (escape_sequence) @constant.character.escape 104 | 105 | ; Comments 106 | 107 | (comment) @spell @comment.line 108 | 109 | (shebang) @keyword.directive 110 | 111 | ; highlight known settings (filtering does not always work) 112 | (setting 113 | left: (identifier) @keyword 114 | (#any-of? @keyword 115 | "allow-duplicate-recipes" 116 | "allow-duplicate-variables" 117 | "dotenv-filename" 118 | "dotenv-load" 119 | "dotenv-path" 120 | "dotenv-required" 121 | "export" 122 | "fallback" 123 | "ignore-comments" 124 | "positional-arguments" 125 | "shell" 126 | "shell-interpreter" 127 | "tempdir" 128 | "windows-powershell" 129 | "windows-shell" 130 | "working-directory")) 131 | 132 | ; highlight known attributes (filtering does not always work) 133 | (attribute 134 | (identifier) @attribute 135 | (#any-of? @attribute 136 | "confirm" 137 | "doc" 138 | "extension" 139 | "group" 140 | "linux" 141 | "macos" 142 | "no-cd" 143 | "no-exit-message" 144 | "no-quiet" 145 | "positional-arguments" 146 | "private" 147 | "script" 148 | "unix" 149 | "windows")) 150 | 151 | ; Numbers are part of the syntax tree, even if disallowed 152 | (numeric_error) @error 153 | -------------------------------------------------------------------------------- /queries-src/indents.scm: -------------------------------------------------------------------------------- 1 | ; This query specifies how to auto-indent logical blocks. 2 | ; 3 | ; Better documentation with diagrams is in https://docs.helix-editor.com/guides/indent.html SKIP-NVIM SKIP-NVIM-NEXT 4 | 5 | [ 6 | (recipe) 7 | (string) 8 | (external_command) 9 | ] @indent @extend 10 | 11 | (comment) @indent.auto ; SKIP-HELIX SKIP-ZED SKIP-LAPCE 12 | -------------------------------------------------------------------------------- /queries-src/injections.scm: -------------------------------------------------------------------------------- 1 | ; Specify nested languages that live within a `justfile` 2 | 3 | ; FIXME: these are not compatible with helix due to precedence SKIP-NVIM SKIP-NVIM-OLD 4 | 5 | ; ================ Always applicable ================ 6 | 7 | ((comment) @injection.content 8 | (#set! injection.language "comment")) 9 | 10 | ; Highlight the RHS of `=~` as regex 11 | ((regex_literal 12 | (_) @injection.content) 13 | (#set! injection.language "regex")) 14 | 15 | ; ================ Global defaults ================ 16 | 17 | ; Default everything to be bash 18 | (recipe_body 19 | !shebang 20 | (#set! injection.language "bash") 21 | (#set! injection.include-children)) @injection.content 22 | 23 | (external_command 24 | (command_body) @injection.content 25 | (#set! injection.language "bash")) 26 | 27 | ; ================ Global language specified ================ 28 | ; Global language is set with something like one of the following: 29 | ; 30 | ; set shell := ["bash", "-c", ...] 31 | ; set shell := ["pwsh.exe"] 32 | ; 33 | ; We can extract the first item of the array, but we can't extract the language 34 | ; name from the string with something like regex. So instead we special case 35 | ; two things: powershell, which is likely to come with a `.exe` attachment that 36 | ; we need to strip, and everything else which hopefully has no extension. We 37 | ; separate this with a `#match?`. 38 | ; 39 | ; Unfortunately, there also isn't a way to allow arbitrary nesting or 40 | ; alternatively set "global" capture variables. So we can set this for item- 41 | ; level external commands, but not for e.g. external commands within an 42 | ; expression without getting _really_ annoying. Should at least look fine since 43 | ; they default to bash. Limitations... 44 | ; See https://github.com/tree-sitter/tree-sitter/issues/880 for more on that. 45 | 46 | (source_file 47 | (setting "shell" ":=" "[" (string) @_langstr 48 | (#match? @_langstr ".*(powershell|pwsh|cmd).*") 49 | (#set! injection.language "powershell")) 50 | [ 51 | (recipe 52 | (recipe_body 53 | !shebang 54 | (#set! injection.include-children)) @injection.content) 55 | 56 | (assignment 57 | (expression 58 | (value 59 | (external_command 60 | (command_body) @injection.content)))) 61 | ]) 62 | 63 | (source_file 64 | (setting "shell" ":=" "[" (string) @injection.language 65 | (#not-match? @injection.language ".*(powershell|pwsh|cmd).*")) 66 | [ 67 | (recipe 68 | (recipe_body 69 | !shebang 70 | (#set! injection.include-children)) @injection.content) 71 | 72 | (assignment 73 | (expression 74 | (value 75 | (external_command 76 | (command_body) @injection.content)))) 77 | ]) 78 | 79 | ; ================ Recipe language specified ================ ; SKIP-HELIX 80 | ; SKIP-HELIX 81 | ; Set highlighting for recipes that specify a language, using the exact name by default ; SKIP-HELIX 82 | (recipe_body ; ; SKIP-HELIX 83 | (shebang ; ; SKIP-HELIX 84 | (language) @injection.language) ; SKIP-HELIX 85 | (#not-any-of? @injection.language "python3" "nodejs" "node" "uv") ; SKIP-HELIX 86 | (#set! injection.include-children)) @injection.content ; SKIP-HELIX 87 | ; SKIP-HELIX 88 | ; Transform some known executables ; SKIP-HELIX 89 | ; SKIP-HELIX 90 | ; python3/uv -> python ; SKIP-HELIX 91 | (recipe_body ; SKIP-HELIX 92 | (shebang ; SKIP-HELIX 93 | (language) @_lang) ; SKIP-HELIX 94 | (#any-of? @_lang "python3" "uv") ; SKIP-HELIX 95 | (#set! injection.language "python") ; SKIP-HELIX 96 | (#set! injection.include-children)) @injection.content ; SKIP-HELIX 97 | ; SKIP-HELIX 98 | ; node/nodejs -> javascript ; SKIP-HELIX 99 | (recipe_body ; SKIP-HELIX 100 | (shebang ; SKIP-HELIX 101 | (language) @_lang) ; SKIP-HELIX 102 | (#any-of? @_lang "node" "nodejs") ; SKIP-HELIX 103 | (#set! injection.language "javascript") ; SKIP-HELIX 104 | (#set! injection.include-children)) @injection.content ; SKIP-HELIX 105 | 106 | ; ================ Recipe language specified - Helix only ================ ; SKIP-NVIM SKIP-NVIM-OLD SKIP-LAPCE SKIP-ZED 107 | ; SKIP-NVIM SKIP-NVIM-OLD SKIP-LAPCE SKIP-ZED 108 | ; Set highlighting for recipes that specify a language using builtin shebang matching ; SKIP-NVIM SKIP-NVIM-OLD SKIP-LAPCE SKIP-ZED 109 | (recipe_body ; SKIP-NVIM SKIP-NVIM-OLD SKIP-LAPCE SKIP-ZED 110 | (shebang) @injection.shebang ; SKIP-NVIM SKIP-NVIM-OLD SKIP-LAPCE SKIP-ZED 111 | (#set! injection.include-children)) @injection.content ; SKIP-NVIM SKIP-NVIM-OLD SKIP-LAPCE SKIP-ZED 112 | -------------------------------------------------------------------------------- /queries-src/locals.scm: -------------------------------------------------------------------------------- 1 | ; This file tells us about the scope of variables so e.g. local 2 | ; variables override global functions with the same name 3 | 4 | ; Scope 5 | 6 | (recipe) @local.scope 7 | 8 | ; Definitions 9 | 10 | (alias 11 | left: (identifier) @local.definition.variable) 12 | 13 | (assignment 14 | left: (identifier) @local.definition.variable) 15 | 16 | (module 17 | name: (identifier) @local.definition.namespace) 18 | 19 | (parameter 20 | name: (identifier) @local.definition.variable) 21 | 22 | (recipe_header 23 | name: (identifier) @local.definition.function) 24 | 25 | ; References 26 | 27 | (alias 28 | right: (identifier) @local.reference.variable) 29 | 30 | (function_call 31 | name: (identifier) @local.reference.function) 32 | 33 | (dependency 34 | name: (identifier) @local.reference.function) 35 | 36 | (dependency_expression 37 | name: (identifier) @local.reference.function) 38 | 39 | (value 40 | (identifier) @local.reference.variable) 41 | -------------------------------------------------------------------------------- /queries-src/textobjects.scm: -------------------------------------------------------------------------------- 1 | ; Specify how to navigate around logical blocks in code 2 | 3 | (recipe 4 | (recipe_body) @function.inside) @function.around 5 | 6 | (parameters 7 | ((_) @parameter.inside . ","? @parameter.around)) @parameter.around 8 | 9 | (dependency_expression 10 | (_) @parameter.inside) @parameter.around 11 | 12 | (function_call 13 | arguments: (sequence 14 | (expression) @parameter.inside) @parameter.around) @function.around 15 | 16 | (comment) @comment.around 17 | -------------------------------------------------------------------------------- /queries/just/folds.scm: -------------------------------------------------------------------------------- 1 | ; File autogenerated by build-queries-nvim.py; do not edit 2 | 3 | ; Define collapse points 4 | 5 | ([ 6 | (recipe) 7 | (string) 8 | (external_command) 9 | ] @fold 10 | (#trim! @fold)) 11 | -------------------------------------------------------------------------------- /queries/just/highlights.scm: -------------------------------------------------------------------------------- 1 | ; File autogenerated by build-queries-nvim.py; do not edit 2 | 3 | ; This file specifies how matched syntax patterns should be highlighted 4 | 5 | [ 6 | "export" 7 | "import" 8 | ] @keyword.import 9 | 10 | "mod" @module 11 | 12 | [ 13 | "alias" 14 | "set" 15 | "shell" 16 | ] @keyword 17 | 18 | [ 19 | "if" 20 | "else" 21 | ] @keyword.conditional 22 | 23 | ; Variables 24 | 25 | (value 26 | (identifier) @variable) 27 | 28 | (alias 29 | left: (identifier) @variable) 30 | 31 | (assignment 32 | left: (identifier) @variable) 33 | 34 | ; Functions 35 | 36 | (recipe_header 37 | name: (identifier) @function) 38 | 39 | (dependency 40 | name: (identifier) @function.call) 41 | 42 | (dependency_expression 43 | name: (identifier) @function.call) 44 | 45 | (function_call 46 | name: (identifier) @function.call) 47 | 48 | ; Parameters 49 | 50 | (parameter 51 | name: (identifier) @variable.parameter) 52 | 53 | ; Namespaces 54 | 55 | (module 56 | name: (identifier) @module) 57 | 58 | ; Operators 59 | 60 | [ 61 | ":=" 62 | "?" 63 | "==" 64 | "!=" 65 | "=~" 66 | "@" 67 | "=" 68 | "$" 69 | "*" 70 | "+" 71 | "&&" 72 | "@-" 73 | "-@" 74 | "-" 75 | "/" 76 | ":" 77 | ] @operator 78 | 79 | ; Punctuation 80 | 81 | "," @punctuation.delimiter 82 | 83 | [ 84 | "{" 85 | "}" 86 | "[" 87 | "]" 88 | "(" 89 | ")" 90 | "{{" 91 | "}}" 92 | ] @punctuation.bracket 93 | 94 | [ "`" "```" ] @punctuation.special 95 | 96 | ; Literals 97 | 98 | (boolean) @boolean 99 | 100 | [ 101 | (string) 102 | (external_command) 103 | ] @string 104 | 105 | (escape_sequence) @string.escape 106 | 107 | ; Comments 108 | 109 | (comment) @spell @comment 110 | 111 | (shebang) @keyword.directive 112 | 113 | ; highlight known settings (filtering does not always work) 114 | (setting 115 | left: (identifier) @keyword 116 | (#any-of? @keyword 117 | "allow-duplicate-recipes" 118 | "allow-duplicate-variables" 119 | "dotenv-filename" 120 | "dotenv-load" 121 | "dotenv-path" 122 | "dotenv-required" 123 | "export" 124 | "fallback" 125 | "ignore-comments" 126 | "positional-arguments" 127 | "shell" 128 | "shell-interpreter" 129 | "tempdir" 130 | "windows-powershell" 131 | "windows-shell" 132 | "working-directory")) 133 | 134 | ; highlight known attributes (filtering does not always work) 135 | (attribute 136 | (identifier) @attribute 137 | (#any-of? @attribute 138 | "confirm" 139 | "doc" 140 | "extension" 141 | "group" 142 | "linux" 143 | "macos" 144 | "no-cd" 145 | "no-exit-message" 146 | "no-quiet" 147 | "positional-arguments" 148 | "private" 149 | "script" 150 | "unix" 151 | "windows")) 152 | 153 | ; Numbers are part of the syntax tree, even if disallowed 154 | (numeric_error) @error 155 | -------------------------------------------------------------------------------- /queries/just/indents.scm: -------------------------------------------------------------------------------- 1 | ; File autogenerated by build-queries-nvim.py; do not edit 2 | 3 | ; This query specifies how to auto-indent logical blocks. 4 | ; 5 | 6 | [ 7 | (recipe) 8 | (string) 9 | (external_command) 10 | ] @indent.begin 11 | 12 | (comment) @indent.auto 13 | -------------------------------------------------------------------------------- /queries/just/injections.scm: -------------------------------------------------------------------------------- 1 | ; File autogenerated by build-queries-nvim.py; do not edit 2 | 3 | ; Specify nested languages that live within a `justfile` 4 | 5 | ; ================ Always applicable ================ 6 | 7 | ((comment) @injection.content 8 | (#set! injection.language "comment")) 9 | 10 | ; Highlight the RHS of `=~` as regex 11 | ((regex_literal 12 | (_) @injection.content) 13 | (#set! injection.language "regex")) 14 | 15 | ; ================ Global defaults ================ 16 | 17 | ; Default everything to be bash 18 | (recipe_body 19 | !shebang 20 | (#set! injection.language "bash") 21 | (#set! injection.include-children)) @injection.content 22 | 23 | (external_command 24 | (command_body) @injection.content 25 | (#set! injection.language "bash")) 26 | 27 | ; ================ Global language specified ================ 28 | ; Global language is set with something like one of the following: 29 | ; 30 | ; set shell := ["bash", "-c", ...] 31 | ; set shell := ["pwsh.exe"] 32 | ; 33 | ; We can extract the first item of the array, but we can't extract the language 34 | ; name from the string with something like regex. So instead we special case 35 | ; two things: powershell, which is likely to come with a `.exe` attachment that 36 | ; we need to strip, and everything else which hopefully has no extension. We 37 | ; separate this with a `#match?`. 38 | ; 39 | ; Unfortunately, there also isn't a way to allow arbitrary nesting or 40 | ; alternatively set "global" capture variables. So we can set this for item- 41 | ; level external commands, but not for e.g. external commands within an 42 | ; expression without getting _really_ annoying. Should at least look fine since 43 | ; they default to bash. Limitations... 44 | ; See https://github.com/tree-sitter/tree-sitter/issues/880 for more on that. 45 | 46 | (source_file 47 | (setting "shell" ":=" "[" (string) @_langstr 48 | (#match? @_langstr ".*(powershell|pwsh|cmd).*") 49 | (#set! injection.language "powershell")) 50 | [ 51 | (recipe 52 | (recipe_body 53 | !shebang 54 | (#set! injection.include-children)) @injection.content) 55 | 56 | (assignment 57 | (expression 58 | (value 59 | (external_command 60 | (command_body) @injection.content)))) 61 | ]) 62 | 63 | (source_file 64 | (setting "shell" ":=" "[" (string) @injection.language 65 | (#not-match? @injection.language ".*(powershell|pwsh|cmd).*")) 66 | [ 67 | (recipe 68 | (recipe_body 69 | !shebang 70 | (#set! injection.include-children)) @injection.content) 71 | 72 | (assignment 73 | (expression 74 | (value 75 | (external_command 76 | (command_body) @injection.content)))) 77 | ]) 78 | 79 | ; ================ Recipe language specified ================ 80 | 81 | ; Set highlighting for recipes that specify a language, using the exact name by default 82 | (recipe_body ; 83 | (shebang ; 84 | (language) @injection.language) 85 | (#not-any-of? @injection.language "python3" "nodejs" "node" "uv") 86 | (#set! injection.include-children)) @injection.content 87 | 88 | ; Transform some known executables 89 | 90 | ; python3/uv -> python 91 | (recipe_body 92 | (shebang 93 | (language) @_lang) 94 | (#any-of? @_lang "python3" "uv") 95 | (#set! injection.language "python") 96 | (#set! injection.include-children)) @injection.content 97 | 98 | ; node/nodejs -> javascript 99 | (recipe_body 100 | (shebang 101 | (language) @_lang) 102 | (#any-of? @_lang "node" "nodejs") 103 | (#set! injection.language "javascript") 104 | (#set! injection.include-children)) @injection.content 105 | -------------------------------------------------------------------------------- /queries/just/locals.scm: -------------------------------------------------------------------------------- 1 | ; File autogenerated by build-queries-nvim.py; do not edit 2 | 3 | ; This file tells us about the scope of variables so e.g. local 4 | ; variables override global functions with the same name 5 | 6 | ; Scope 7 | 8 | (recipe) @local.scope 9 | 10 | ; Definitions 11 | 12 | (alias 13 | left: (identifier) @local.definition.var) 14 | 15 | (assignment 16 | left: (identifier) @local.definition.var) 17 | 18 | (module 19 | name: (identifier) @local.definition.namespace) 20 | 21 | (parameter 22 | name: (identifier) @local.definition.var) 23 | 24 | (recipe_header 25 | name: (identifier) @local.definition.function) 26 | 27 | ; References 28 | 29 | (alias 30 | right: (identifier) @local.reference) 31 | 32 | (function_call 33 | name: (identifier) @local.reference) 34 | 35 | (dependency 36 | name: (identifier) @local.reference) 37 | 38 | (dependency_expression 39 | name: (identifier) @local.reference) 40 | 41 | (value 42 | (identifier) @local.reference) 43 | -------------------------------------------------------------------------------- /queries/just/textobjects.scm: -------------------------------------------------------------------------------- 1 | ; File autogenerated by build-queries-nvim.py; do not edit 2 | 3 | ; Specify how to navigate around logical blocks in code 4 | 5 | (recipe 6 | (recipe_body) @function.inner) @function.outer 7 | 8 | (parameters 9 | ((_) @parameter.inner . ","? @parameter.outer)) @parameter.outer 10 | 11 | (dependency_expression 12 | (_) @parameter.inner) @parameter.outer 13 | 14 | (function_call 15 | arguments: (sequence 16 | (expression) @parameter.inner) @parameter.outer) @function.outer 17 | 18 | (comment) @comment.outer 19 | -------------------------------------------------------------------------------- /src/scanner.c: -------------------------------------------------------------------------------- 1 | #include "tree_sitter/parser.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #ifdef NDEBUG 9 | #error "expected assertions to be enabled" 10 | #endif 11 | 12 | // Enable this for debugging 13 | // #define DEBUG_PRINT 14 | 15 | #ifndef __FILE_NAME__ 16 | #define __FILE_NAME__ __FILE__ 17 | #endif 18 | 19 | #ifdef __GNUC__ 20 | #define unused_attr __attribute__((unused)) 21 | #else 22 | #define unused_attr 23 | #endif 24 | 25 | #ifdef __wasm__ 26 | #define assertf(...) (void)0; 27 | #else 28 | 29 | #ifndef fprintf_s 30 | #define fprintf_s fprintf // NOLINT 31 | #endif 32 | 33 | #ifdef DEBUG_PRINT 34 | #define dbg_print(...) \ 35 | do { \ 36 | fprintf_s(stderr, " \033[96;1mparse: \033[0m"); \ 37 | fprintf_s(stderr, __VA_ARGS__); \ 38 | } while (0) 39 | #else 40 | #define dbg_print(...) 41 | #endif 42 | 43 | #define panic(...) \ 44 | do { \ 45 | fprintf_s(stderr, "panic at %s:%d: ", __FILE_NAME__, __LINE__); \ 46 | fprintf_s(stderr, __VA_ARGS__); \ 47 | fprintf_s(stderr, "\n"); \ 48 | exit(1); \ 49 | } while (0); 50 | 51 | #define assertf(condition, ...) \ 52 | do { \ 53 | if (__builtin_expect(!(condition), 0)) { \ 54 | panic(__VA_ARGS__); \ 55 | } \ 56 | } while (0); 57 | 58 | #ifndef __GNUC__ 59 | #define __builtin_expect(a, b) a 60 | #endif 61 | 62 | #endif 63 | 64 | #define SBYTES sizeof(Scanner) 65 | 66 | enum TokenType { 67 | INDENT, 68 | DEDENT, 69 | NEWLINE, 70 | TEXT, 71 | ERROR_RECOVERY, 72 | TOKEN_TYPE_END, 73 | }; 74 | 75 | unused_attr static inline void assert_valid_token(const TSSymbol sym) { 76 | assertf(sym >= INDENT && sym < TOKEN_TYPE_END, "invalid symbol %d", sym); 77 | } 78 | 79 | typedef struct Scanner { 80 | uint32_t prev_indent; 81 | uint16_t advance_brace_count; 82 | bool has_seen_eof; 83 | } Scanner; 84 | 85 | // This function should create your scanner object. It will only be called once 86 | // anytime your language is set on a parser. Often, you will want to allocate 87 | // memory on the heap and return a pointer to it. If your external scanner 88 | // doesn’t need to maintain any state, it’s ok to return NULL. 89 | void *tree_sitter_just_external_scanner_create(void) { 90 | Scanner *ptr = (Scanner *)calloc(SBYTES, 1); 91 | assertf(ptr, "tree_sitter_just_external_scanner_create: out of memory"); 92 | return ptr; 93 | } 94 | 95 | // This function should free any memory used by your scanner. It is called once 96 | // when a parser is deleted or assigned a different language. It receives as an 97 | // argument the same pointer that was returned from the create function. If your 98 | // create function didn’t allocate any memory, this function can be a noop. 99 | void tree_sitter_just_external_scanner_destroy(void *payload) { 100 | assertf(payload, "got null payload at destroy"); 101 | free(payload); 102 | } 103 | 104 | // Serialize the state of the scanner. This is called when the parser is 105 | // serialized. It receives as an argument the same pointer that was returned 106 | // from the create function. 107 | unsigned tree_sitter_just_external_scanner_serialize(void *payload, 108 | char *buffer) { 109 | assertf(SBYTES < TREE_SITTER_SERIALIZATION_BUFFER_SIZE, 110 | "invalid scanner size"); 111 | memcpy(buffer, payload, SBYTES); 112 | return SBYTES; 113 | } 114 | 115 | // Reconstruct a scanner from the serialized state. This is called when the 116 | // parser is deserialized. 117 | void tree_sitter_just_external_scanner_deserialize(void *payload, 118 | const char *buffer, 119 | unsigned length) { 120 | Scanner *ptr = (Scanner *)payload; 121 | if (length == 0) { 122 | ptr->prev_indent = 0; 123 | ptr->has_seen_eof = false; 124 | return; 125 | } 126 | memcpy(ptr, buffer, SBYTES); 127 | } 128 | 129 | // Continue and include the preceding character in the token 130 | static inline void advance(TSLexer *lexer) { lexer->advance(lexer, false); } 131 | 132 | // Continue and discard the preceding character 133 | static inline void skip(TSLexer *lexer) { lexer->advance(lexer, true); } 134 | 135 | // An EOF works as a dedent 136 | static bool handle_eof(TSLexer *lexer, Scanner *state, 137 | const bool *valid_symbols) { 138 | assertf(lexer->eof(lexer), "expected EOF"); 139 | lexer->mark_end(lexer); 140 | 141 | if (valid_symbols[DEDENT]) { 142 | lexer->result_symbol = DEDENT; 143 | return true; 144 | } 145 | 146 | if (valid_symbols[NEWLINE]) { 147 | if (state->has_seen_eof) { 148 | // allow EOF to count for a single symbol. Don't return true more than 149 | // once, otherwise it will keep calling us thinking there are more tokens. 150 | return false; 151 | } 152 | 153 | lexer->result_symbol = NEWLINE; 154 | state->has_seen_eof = true; 155 | return true; 156 | } 157 | return false; 158 | } 159 | 160 | // This function is responsible for recognizing external tokens. It should 161 | // return true if a token was recognized, and false otherwise. 162 | bool tree_sitter_just_external_scanner_scan(void *payload, TSLexer *lexer, 163 | const bool *valid_symbols) { 164 | Scanner *scanner = (Scanner *)(payload); 165 | 166 | if (lexer->eof(lexer)) { 167 | return handle_eof(lexer, scanner, valid_symbols); 168 | } 169 | 170 | // Handle backslash escaping for newlines 171 | if (valid_symbols[NEWLINE]) { 172 | bool escape = false; 173 | if (lexer->lookahead == '\\') { 174 | escape = true; 175 | skip(lexer); 176 | } 177 | 178 | bool eol_found = false; 179 | while (iswspace(lexer->lookahead)) { 180 | if (lexer->lookahead == '\n') { 181 | skip(lexer); 182 | eol_found = true; 183 | break; 184 | } 185 | skip(lexer); 186 | } 187 | 188 | if (eol_found && !escape) { 189 | lexer->result_symbol = NEWLINE; 190 | return true; 191 | } 192 | } 193 | 194 | if (valid_symbols[INDENT] || valid_symbols[DEDENT]) { 195 | while (!lexer->eof(lexer) && isspace(lexer->lookahead)) { 196 | switch (lexer->lookahead) { 197 | case '\n': 198 | if (valid_symbols[INDENT]) { 199 | return false; 200 | } 201 | 202 | case '\t': 203 | case ' ': 204 | skip(lexer); 205 | break; 206 | 207 | default: 208 | return false; 209 | } 210 | } 211 | 212 | if (lexer->eof(lexer)) { 213 | return handle_eof(lexer, scanner, valid_symbols); 214 | } 215 | 216 | uint32_t indent = lexer->get_column(lexer); 217 | 218 | if (indent > scanner->prev_indent && valid_symbols[INDENT] && 219 | scanner->prev_indent == 0) { 220 | lexer->result_symbol = INDENT; 221 | scanner->prev_indent = indent; 222 | return true; 223 | } 224 | if (indent < scanner->prev_indent && valid_symbols[DEDENT] && indent == 0) { 225 | lexer->result_symbol = DEDENT; 226 | scanner->prev_indent = indent; 227 | return true; 228 | } 229 | } 230 | 231 | if (valid_symbols[TEXT]) { 232 | if (lexer->get_column(lexer) == scanner->prev_indent && 233 | (lexer->lookahead == '\n' || lexer->lookahead == '@' || 234 | lexer->lookahead == '-')) { 235 | return false; 236 | } 237 | 238 | bool advanced_once = false; 239 | 240 | while (lexer->lookahead == '{' && scanner->advance_brace_count > 0 && 241 | !lexer->eof(lexer)) { 242 | scanner->advance_brace_count--; 243 | advance(lexer); 244 | advanced_once = true; 245 | } 246 | 247 | while (1) { 248 | if (lexer->eof(lexer)) { 249 | return handle_eof(lexer, scanner, valid_symbols); 250 | } 251 | 252 | while (!lexer->eof(lexer) && lexer->lookahead != '\n' && 253 | lexer->lookahead != '{') { 254 | // Can't start with #! 255 | if (lexer->lookahead == '#' && !advanced_once) { 256 | advance(lexer); 257 | if (lexer->lookahead == '!') { 258 | return false; 259 | } 260 | } 261 | 262 | advance(lexer); 263 | advanced_once = true; 264 | } 265 | 266 | if (lexer->lookahead == '\n' || lexer->eof(lexer)) { 267 | lexer->mark_end(lexer); 268 | lexer->result_symbol = TEXT; 269 | if (advanced_once) { 270 | return true; 271 | } 272 | if (lexer->eof(lexer)) { 273 | return handle_eof(lexer, scanner, valid_symbols); 274 | } 275 | advance(lexer); 276 | } else if (lexer->lookahead == '{') { 277 | lexer->mark_end(lexer); 278 | advance(lexer); 279 | 280 | if (lexer->eof(lexer) || 281 | lexer->lookahead == '\n') { // EOF without anything after { 282 | lexer->mark_end(lexer); 283 | lexer->result_symbol = TEXT; 284 | return advanced_once; 285 | } 286 | 287 | if (lexer->lookahead == '{') { 288 | advance(lexer); 289 | 290 | while (lexer->lookahead == '{') { // more braces! 291 | scanner->advance_brace_count++; 292 | advance(lexer); 293 | } 294 | 295 | // scan till a balanced pair of }} are found, then assume it's a valid 296 | // interpolation 297 | while (!lexer->eof(lexer) && lexer->lookahead != '\n') { 298 | advance(lexer); 299 | if (lexer->lookahead == '}') { 300 | advance(lexer); 301 | if (lexer->lookahead == '}') { 302 | lexer->result_symbol = TEXT; 303 | return advanced_once; 304 | } 305 | } 306 | } 307 | 308 | if (!advanced_once) { 309 | return false; 310 | } 311 | } 312 | } 313 | } 314 | } 315 | 316 | return false; 317 | } 318 | -------------------------------------------------------------------------------- /src/tree_sitter/alloc.h: -------------------------------------------------------------------------------- 1 | #ifndef TREE_SITTER_ALLOC_H_ 2 | #define TREE_SITTER_ALLOC_H_ 3 | 4 | #ifdef __cplusplus 5 | extern "C" { 6 | #endif 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | // Allow clients to override allocation functions 13 | #ifdef TREE_SITTER_REUSE_ALLOCATOR 14 | 15 | extern void *(*ts_current_malloc)(size_t size); 16 | extern void *(*ts_current_calloc)(size_t count, size_t size); 17 | extern void *(*ts_current_realloc)(void *ptr, size_t size); 18 | extern void (*ts_current_free)(void *ptr); 19 | 20 | #ifndef ts_malloc 21 | #define ts_malloc ts_current_malloc 22 | #endif 23 | #ifndef ts_calloc 24 | #define ts_calloc ts_current_calloc 25 | #endif 26 | #ifndef ts_realloc 27 | #define ts_realloc ts_current_realloc 28 | #endif 29 | #ifndef ts_free 30 | #define ts_free ts_current_free 31 | #endif 32 | 33 | #else 34 | 35 | #ifndef ts_malloc 36 | #define ts_malloc malloc 37 | #endif 38 | #ifndef ts_calloc 39 | #define ts_calloc calloc 40 | #endif 41 | #ifndef ts_realloc 42 | #define ts_realloc realloc 43 | #endif 44 | #ifndef ts_free 45 | #define ts_free free 46 | #endif 47 | 48 | #endif 49 | 50 | #ifdef __cplusplus 51 | } 52 | #endif 53 | 54 | #endif // TREE_SITTER_ALLOC_H_ 55 | -------------------------------------------------------------------------------- /src/tree_sitter/array.h: -------------------------------------------------------------------------------- 1 | #ifndef TREE_SITTER_ARRAY_H_ 2 | #define TREE_SITTER_ARRAY_H_ 3 | 4 | #ifdef __cplusplus 5 | extern "C" { 6 | #endif 7 | 8 | #include "./alloc.h" 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #ifdef _MSC_VER 17 | #pragma warning(disable : 4101) 18 | #elif defined(__GNUC__) || defined(__clang__) 19 | #pragma GCC diagnostic push 20 | #pragma GCC diagnostic ignored "-Wunused-variable" 21 | #endif 22 | 23 | #define Array(T) \ 24 | struct { \ 25 | T *contents; \ 26 | uint32_t size; \ 27 | uint32_t capacity; \ 28 | } 29 | 30 | /// Initialize an array. 31 | #define array_init(self) \ 32 | ((self)->size = 0, (self)->capacity = 0, (self)->contents = NULL) 33 | 34 | /// Create an empty array. 35 | #define array_new() \ 36 | { NULL, 0, 0 } 37 | 38 | /// Get a pointer to the element at a given `index` in the array. 39 | #define array_get(self, _index) \ 40 | (assert((uint32_t)(_index) < (self)->size), &(self)->contents[_index]) 41 | 42 | /// Get a pointer to the first element in the array. 43 | #define array_front(self) array_get(self, 0) 44 | 45 | /// Get a pointer to the last element in the array. 46 | #define array_back(self) array_get(self, (self)->size - 1) 47 | 48 | /// Clear the array, setting its size to zero. Note that this does not free any 49 | /// memory allocated for the array's contents. 50 | #define array_clear(self) ((self)->size = 0) 51 | 52 | /// Reserve `new_capacity` elements of space in the array. If `new_capacity` is 53 | /// less than the array's current capacity, this function has no effect. 54 | #define array_reserve(self, new_capacity) \ 55 | _array__reserve((Array *)(self), array_elem_size(self), new_capacity) 56 | 57 | /// Free any memory allocated for this array. Note that this does not free any 58 | /// memory allocated for the array's contents. 59 | #define array_delete(self) _array__delete((Array *)(self)) 60 | 61 | /// Push a new `element` onto the end of the array. 62 | #define array_push(self, element) \ 63 | (_array__grow((Array *)(self), 1, array_elem_size(self)), \ 64 | (self)->contents[(self)->size++] = (element)) 65 | 66 | /// Increase the array's size by `count` elements. 67 | /// New elements are zero-initialized. 68 | #define array_grow_by(self, count) \ 69 | do { \ 70 | if ((count) == 0) break; \ 71 | _array__grow((Array *)(self), count, array_elem_size(self)); \ 72 | memset((self)->contents + (self)->size, 0, (count) * array_elem_size(self)); \ 73 | (self)->size += (count); \ 74 | } while (0) 75 | 76 | /// Append all elements from one array to the end of another. 77 | #define array_push_all(self, other) \ 78 | array_extend((self), (other)->size, (other)->contents) 79 | 80 | /// Append `count` elements to the end of the array, reading their values from the 81 | /// `contents` pointer. 82 | #define array_extend(self, count, contents) \ 83 | _array__splice( \ 84 | (Array *)(self), array_elem_size(self), (self)->size, \ 85 | 0, count, contents \ 86 | ) 87 | 88 | /// Remove `old_count` elements from the array starting at the given `index`. At 89 | /// the same index, insert `new_count` new elements, reading their values from the 90 | /// `new_contents` pointer. 91 | #define array_splice(self, _index, old_count, new_count, new_contents) \ 92 | _array__splice( \ 93 | (Array *)(self), array_elem_size(self), _index, \ 94 | old_count, new_count, new_contents \ 95 | ) 96 | 97 | /// Insert one `element` into the array at the given `index`. 98 | #define array_insert(self, _index, element) \ 99 | _array__splice((Array *)(self), array_elem_size(self), _index, 0, 1, &(element)) 100 | 101 | /// Remove one element from the array at the given `index`. 102 | #define array_erase(self, _index) \ 103 | _array__erase((Array *)(self), array_elem_size(self), _index) 104 | 105 | /// Pop the last element off the array, returning the element by value. 106 | #define array_pop(self) ((self)->contents[--(self)->size]) 107 | 108 | /// Assign the contents of one array to another, reallocating if necessary. 109 | #define array_assign(self, other) \ 110 | _array__assign((Array *)(self), (const Array *)(other), array_elem_size(self)) 111 | 112 | /// Swap one array with another 113 | #define array_swap(self, other) \ 114 | _array__swap((Array *)(self), (Array *)(other)) 115 | 116 | /// Get the size of the array contents 117 | #define array_elem_size(self) (sizeof *(self)->contents) 118 | 119 | /// Search a sorted array for a given `needle` value, using the given `compare` 120 | /// callback to determine the order. 121 | /// 122 | /// If an existing element is found to be equal to `needle`, then the `index` 123 | /// out-parameter is set to the existing value's index, and the `exists` 124 | /// out-parameter is set to true. Otherwise, `index` is set to an index where 125 | /// `needle` should be inserted in order to preserve the sorting, and `exists` 126 | /// is set to false. 127 | #define array_search_sorted_with(self, compare, needle, _index, _exists) \ 128 | _array__search_sorted(self, 0, compare, , needle, _index, _exists) 129 | 130 | /// Search a sorted array for a given `needle` value, using integer comparisons 131 | /// of a given struct field (specified with a leading dot) to determine the order. 132 | /// 133 | /// See also `array_search_sorted_with`. 134 | #define array_search_sorted_by(self, field, needle, _index, _exists) \ 135 | _array__search_sorted(self, 0, _compare_int, field, needle, _index, _exists) 136 | 137 | /// Insert a given `value` into a sorted array, using the given `compare` 138 | /// callback to determine the order. 139 | #define array_insert_sorted_with(self, compare, value) \ 140 | do { \ 141 | unsigned _index, _exists; \ 142 | array_search_sorted_with(self, compare, &(value), &_index, &_exists); \ 143 | if (!_exists) array_insert(self, _index, value); \ 144 | } while (0) 145 | 146 | /// Insert a given `value` into a sorted array, using integer comparisons of 147 | /// a given struct field (specified with a leading dot) to determine the order. 148 | /// 149 | /// See also `array_search_sorted_by`. 150 | #define array_insert_sorted_by(self, field, value) \ 151 | do { \ 152 | unsigned _index, _exists; \ 153 | array_search_sorted_by(self, field, (value) field, &_index, &_exists); \ 154 | if (!_exists) array_insert(self, _index, value); \ 155 | } while (0) 156 | 157 | // Private 158 | 159 | typedef Array(void) Array; 160 | 161 | /// This is not what you're looking for, see `array_delete`. 162 | static inline void _array__delete(Array *self) { 163 | if (self->contents) { 164 | ts_free(self->contents); 165 | self->contents = NULL; 166 | self->size = 0; 167 | self->capacity = 0; 168 | } 169 | } 170 | 171 | /// This is not what you're looking for, see `array_erase`. 172 | static inline void _array__erase(Array *self, size_t element_size, 173 | uint32_t index) { 174 | assert(index < self->size); 175 | char *contents = (char *)self->contents; 176 | memmove(contents + index * element_size, contents + (index + 1) * element_size, 177 | (self->size - index - 1) * element_size); 178 | self->size--; 179 | } 180 | 181 | /// This is not what you're looking for, see `array_reserve`. 182 | static inline void _array__reserve(Array *self, size_t element_size, uint32_t new_capacity) { 183 | if (new_capacity > self->capacity) { 184 | if (self->contents) { 185 | self->contents = ts_realloc(self->contents, new_capacity * element_size); 186 | } else { 187 | self->contents = ts_malloc(new_capacity * element_size); 188 | } 189 | self->capacity = new_capacity; 190 | } 191 | } 192 | 193 | /// This is not what you're looking for, see `array_assign`. 194 | static inline void _array__assign(Array *self, const Array *other, size_t element_size) { 195 | _array__reserve(self, element_size, other->size); 196 | self->size = other->size; 197 | memcpy(self->contents, other->contents, self->size * element_size); 198 | } 199 | 200 | /// This is not what you're looking for, see `array_swap`. 201 | static inline void _array__swap(Array *self, Array *other) { 202 | Array swap = *other; 203 | *other = *self; 204 | *self = swap; 205 | } 206 | 207 | /// This is not what you're looking for, see `array_push` or `array_grow_by`. 208 | static inline void _array__grow(Array *self, uint32_t count, size_t element_size) { 209 | uint32_t new_size = self->size + count; 210 | if (new_size > self->capacity) { 211 | uint32_t new_capacity = self->capacity * 2; 212 | if (new_capacity < 8) new_capacity = 8; 213 | if (new_capacity < new_size) new_capacity = new_size; 214 | _array__reserve(self, element_size, new_capacity); 215 | } 216 | } 217 | 218 | /// This is not what you're looking for, see `array_splice`. 219 | static inline void _array__splice(Array *self, size_t element_size, 220 | uint32_t index, uint32_t old_count, 221 | uint32_t new_count, const void *elements) { 222 | uint32_t new_size = self->size + new_count - old_count; 223 | uint32_t old_end = index + old_count; 224 | uint32_t new_end = index + new_count; 225 | assert(old_end <= self->size); 226 | 227 | _array__reserve(self, element_size, new_size); 228 | 229 | char *contents = (char *)self->contents; 230 | if (self->size > old_end) { 231 | memmove( 232 | contents + new_end * element_size, 233 | contents + old_end * element_size, 234 | (self->size - old_end) * element_size 235 | ); 236 | } 237 | if (new_count > 0) { 238 | if (elements) { 239 | memcpy( 240 | (contents + index * element_size), 241 | elements, 242 | new_count * element_size 243 | ); 244 | } else { 245 | memset( 246 | (contents + index * element_size), 247 | 0, 248 | new_count * element_size 249 | ); 250 | } 251 | } 252 | self->size += new_count - old_count; 253 | } 254 | 255 | /// A binary search routine, based on Rust's `std::slice::binary_search_by`. 256 | /// This is not what you're looking for, see `array_search_sorted_with` or `array_search_sorted_by`. 257 | #define _array__search_sorted(self, start, compare, suffix, needle, _index, _exists) \ 258 | do { \ 259 | *(_index) = start; \ 260 | *(_exists) = false; \ 261 | uint32_t size = (self)->size - *(_index); \ 262 | if (size == 0) break; \ 263 | int comparison; \ 264 | while (size > 1) { \ 265 | uint32_t half_size = size / 2; \ 266 | uint32_t mid_index = *(_index) + half_size; \ 267 | comparison = compare(&((self)->contents[mid_index] suffix), (needle)); \ 268 | if (comparison <= 0) *(_index) = mid_index; \ 269 | size -= half_size; \ 270 | } \ 271 | comparison = compare(&((self)->contents[*(_index)] suffix), (needle)); \ 272 | if (comparison == 0) *(_exists) = true; \ 273 | else if (comparison < 0) *(_index) += 1; \ 274 | } while (0) 275 | 276 | /// Helper macro for the `_sorted_by` routines below. This takes the left (existing) 277 | /// parameter by reference in order to work with the generic sorting function above. 278 | #define _compare_int(a, b) ((int)*(a) - (int)(b)) 279 | 280 | #ifdef _MSC_VER 281 | #pragma warning(default : 4101) 282 | #elif defined(__GNUC__) || defined(__clang__) 283 | #pragma GCC diagnostic pop 284 | #endif 285 | 286 | #ifdef __cplusplus 287 | } 288 | #endif 289 | 290 | #endif // TREE_SITTER_ARRAY_H_ 291 | -------------------------------------------------------------------------------- /src/tree_sitter/parser.h: -------------------------------------------------------------------------------- 1 | #ifndef TREE_SITTER_PARSER_H_ 2 | #define TREE_SITTER_PARSER_H_ 3 | 4 | #ifdef __cplusplus 5 | extern "C" { 6 | #endif 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | #define ts_builtin_sym_error ((TSSymbol)-1) 13 | #define ts_builtin_sym_end 0 14 | #define TREE_SITTER_SERIALIZATION_BUFFER_SIZE 1024 15 | 16 | #ifndef TREE_SITTER_API_H_ 17 | typedef uint16_t TSStateId; 18 | typedef uint16_t TSSymbol; 19 | typedef uint16_t TSFieldId; 20 | typedef struct TSLanguage TSLanguage; 21 | #endif 22 | 23 | typedef struct { 24 | TSFieldId field_id; 25 | uint8_t child_index; 26 | bool inherited; 27 | } TSFieldMapEntry; 28 | 29 | typedef struct { 30 | uint16_t index; 31 | uint16_t length; 32 | } TSFieldMapSlice; 33 | 34 | typedef struct { 35 | bool visible; 36 | bool named; 37 | bool supertype; 38 | } TSSymbolMetadata; 39 | 40 | typedef struct TSLexer TSLexer; 41 | 42 | struct TSLexer { 43 | int32_t lookahead; 44 | TSSymbol result_symbol; 45 | void (*advance)(TSLexer *, bool); 46 | void (*mark_end)(TSLexer *); 47 | uint32_t (*get_column)(TSLexer *); 48 | bool (*is_at_included_range_start)(const TSLexer *); 49 | bool (*eof)(const TSLexer *); 50 | void (*log)(const TSLexer *, const char *, ...); 51 | }; 52 | 53 | typedef enum { 54 | TSParseActionTypeShift, 55 | TSParseActionTypeReduce, 56 | TSParseActionTypeAccept, 57 | TSParseActionTypeRecover, 58 | } TSParseActionType; 59 | 60 | typedef union { 61 | struct { 62 | uint8_t type; 63 | TSStateId state; 64 | bool extra; 65 | bool repetition; 66 | } shift; 67 | struct { 68 | uint8_t type; 69 | uint8_t child_count; 70 | TSSymbol symbol; 71 | int16_t dynamic_precedence; 72 | uint16_t production_id; 73 | } reduce; 74 | uint8_t type; 75 | } TSParseAction; 76 | 77 | typedef struct { 78 | uint16_t lex_state; 79 | uint16_t external_lex_state; 80 | } TSLexMode; 81 | 82 | typedef union { 83 | TSParseAction action; 84 | struct { 85 | uint8_t count; 86 | bool reusable; 87 | } entry; 88 | } TSParseActionEntry; 89 | 90 | typedef struct { 91 | int32_t start; 92 | int32_t end; 93 | } TSCharacterRange; 94 | 95 | struct TSLanguage { 96 | uint32_t version; 97 | uint32_t symbol_count; 98 | uint32_t alias_count; 99 | uint32_t token_count; 100 | uint32_t external_token_count; 101 | uint32_t state_count; 102 | uint32_t large_state_count; 103 | uint32_t production_id_count; 104 | uint32_t field_count; 105 | uint16_t max_alias_sequence_length; 106 | const uint16_t *parse_table; 107 | const uint16_t *small_parse_table; 108 | const uint32_t *small_parse_table_map; 109 | const TSParseActionEntry *parse_actions; 110 | const char * const *symbol_names; 111 | const char * const *field_names; 112 | const TSFieldMapSlice *field_map_slices; 113 | const TSFieldMapEntry *field_map_entries; 114 | const TSSymbolMetadata *symbol_metadata; 115 | const TSSymbol *public_symbol_map; 116 | const uint16_t *alias_map; 117 | const TSSymbol *alias_sequences; 118 | const TSLexMode *lex_modes; 119 | bool (*lex_fn)(TSLexer *, TSStateId); 120 | bool (*keyword_lex_fn)(TSLexer *, TSStateId); 121 | TSSymbol keyword_capture_token; 122 | struct { 123 | const bool *states; 124 | const TSSymbol *symbol_map; 125 | void *(*create)(void); 126 | void (*destroy)(void *); 127 | bool (*scan)(void *, TSLexer *, const bool *symbol_whitelist); 128 | unsigned (*serialize)(void *, char *); 129 | void (*deserialize)(void *, const char *, unsigned); 130 | } external_scanner; 131 | const TSStateId *primary_state_ids; 132 | }; 133 | 134 | static inline bool set_contains(TSCharacterRange *ranges, uint32_t len, int32_t lookahead) { 135 | uint32_t index = 0; 136 | uint32_t size = len - index; 137 | while (size > 1) { 138 | uint32_t half_size = size / 2; 139 | uint32_t mid_index = index + half_size; 140 | TSCharacterRange *range = &ranges[mid_index]; 141 | if (lookahead >= range->start && lookahead <= range->end) { 142 | return true; 143 | } else if (lookahead > range->end) { 144 | index = mid_index; 145 | } 146 | size -= half_size; 147 | } 148 | TSCharacterRange *range = &ranges[index]; 149 | return (lookahead >= range->start && lookahead <= range->end); 150 | } 151 | 152 | /* 153 | * Lexer Macros 154 | */ 155 | 156 | #ifdef _MSC_VER 157 | #define UNUSED __pragma(warning(suppress : 4101)) 158 | #else 159 | #define UNUSED __attribute__((unused)) 160 | #endif 161 | 162 | #define START_LEXER() \ 163 | bool result = false; \ 164 | bool skip = false; \ 165 | UNUSED \ 166 | bool eof = false; \ 167 | int32_t lookahead; \ 168 | goto start; \ 169 | next_state: \ 170 | lexer->advance(lexer, skip); \ 171 | start: \ 172 | skip = false; \ 173 | lookahead = lexer->lookahead; 174 | 175 | #define ADVANCE(state_value) \ 176 | { \ 177 | state = state_value; \ 178 | goto next_state; \ 179 | } 180 | 181 | #define ADVANCE_MAP(...) \ 182 | { \ 183 | static const uint16_t map[] = { __VA_ARGS__ }; \ 184 | for (uint32_t i = 0; i < sizeof(map) / sizeof(map[0]); i += 2) { \ 185 | if (map[i] == lookahead) { \ 186 | state = map[i + 1]; \ 187 | goto next_state; \ 188 | } \ 189 | } \ 190 | } 191 | 192 | #define SKIP(state_value) \ 193 | { \ 194 | skip = true; \ 195 | state = state_value; \ 196 | goto next_state; \ 197 | } 198 | 199 | #define ACCEPT_TOKEN(symbol_value) \ 200 | result = true; \ 201 | lexer->result_symbol = symbol_value; \ 202 | lexer->mark_end(lexer); 203 | 204 | #define END_STATE() return result; 205 | 206 | /* 207 | * Parse Table Macros 208 | */ 209 | 210 | #define SMALL_STATE(id) ((id) - LARGE_STATE_COUNT) 211 | 212 | #define STATE(id) id 213 | 214 | #define ACTIONS(id) id 215 | 216 | #define SHIFT(state_value) \ 217 | {{ \ 218 | .shift = { \ 219 | .type = TSParseActionTypeShift, \ 220 | .state = (state_value) \ 221 | } \ 222 | }} 223 | 224 | #define SHIFT_REPEAT(state_value) \ 225 | {{ \ 226 | .shift = { \ 227 | .type = TSParseActionTypeShift, \ 228 | .state = (state_value), \ 229 | .repetition = true \ 230 | } \ 231 | }} 232 | 233 | #define SHIFT_EXTRA() \ 234 | {{ \ 235 | .shift = { \ 236 | .type = TSParseActionTypeShift, \ 237 | .extra = true \ 238 | } \ 239 | }} 240 | 241 | #define REDUCE(symbol_name, children, precedence, prod_id) \ 242 | {{ \ 243 | .reduce = { \ 244 | .type = TSParseActionTypeReduce, \ 245 | .symbol = symbol_name, \ 246 | .child_count = children, \ 247 | .dynamic_precedence = precedence, \ 248 | .production_id = prod_id \ 249 | }, \ 250 | }} 251 | 252 | #define RECOVER() \ 253 | {{ \ 254 | .type = TSParseActionTypeRecover \ 255 | }} 256 | 257 | #define ACCEPT_INPUT() \ 258 | {{ \ 259 | .type = TSParseActionTypeAccept \ 260 | }} 261 | 262 | #ifdef __cplusplus 263 | } 264 | #endif 265 | 266 | #endif // TREE_SITTER_PARSER_H_ 267 | -------------------------------------------------------------------------------- /test/corpus/delimited.txt: -------------------------------------------------------------------------------- 1 | ================================================================================ 2 | basic strings 3 | ================================================================================ 4 | 5 | foo := "a" 6 | bar := "b 7 | c 8 | " 9 | baz := "a\"\t" 10 | 11 | -------------------------------------------------------------------------------- 12 | 13 | (source_file 14 | (assignment 15 | (identifier) 16 | (expression 17 | (value 18 | (string)))) 19 | (assignment 20 | (identifier) 21 | (expression 22 | (value 23 | (string)))) 24 | (assignment 25 | (identifier) 26 | (expression 27 | (value 28 | (string 29 | (escape_sequence) 30 | (escape_sequence)))))) 31 | 32 | ================================================================================ 33 | escape newlines 34 | ================================================================================ 35 | 36 | foo := "a\ 37 | b" 38 | 39 | -------------------------------------------------------------------------------- 40 | 41 | (source_file 42 | (assignment 43 | (identifier) 44 | (expression 45 | (value 46 | (string 47 | (escape_sequence)))))) 48 | 49 | ================================================================================ 50 | indented basic strings 51 | ================================================================================ 52 | 53 | foo := """a""" 54 | bar:= """b 55 | c 56 | """ 57 | baz := """ 58 | abc \t 59 | def \" 60 | ghi \n 61 | jkl 62 | """ 63 | 64 | -------------------------------------------------------------------------------- 65 | 66 | (source_file 67 | (assignment 68 | (identifier) 69 | (expression 70 | (value 71 | (string)))) 72 | (assignment 73 | (identifier) 74 | (expression 75 | (value 76 | (string)))) 77 | (assignment 78 | (identifier) 79 | (expression 80 | (value 81 | (string 82 | (escape_sequence) 83 | (escape_sequence) 84 | (escape_sequence)))))) 85 | 86 | ================================================================================ 87 | indented with nested delim 88 | ================================================================================ 89 | 90 | foo := """ a " b """ 91 | bar:= """b 92 | "baz" 93 | """ 94 | 95 | -------------------------------------------------------------------------------- 96 | 97 | (source_file 98 | (assignment 99 | (identifier) 100 | (expression 101 | (value 102 | (string)))) 103 | (assignment 104 | (identifier) 105 | (expression 106 | (value 107 | (string))))) 108 | 109 | ================================================================================ 110 | raw strings 111 | ================================================================================ 112 | 113 | foo := 'a' 114 | bar := 'b 115 | c 116 | ' 117 | baz := 'a\"\t' 118 | 119 | -------------------------------------------------------------------------------- 120 | 121 | (source_file 122 | (assignment 123 | (identifier) 124 | (expression 125 | (value 126 | (string)))) 127 | (assignment 128 | (identifier) 129 | (expression 130 | (value 131 | (string)))) 132 | (assignment 133 | (identifier) 134 | (expression 135 | (value 136 | (string))))) 137 | 138 | ================================================================================ 139 | indented raw strings 140 | ================================================================================ 141 | 142 | foo := '''a''' 143 | bar:= '''b 144 | c 145 | ''' 146 | baz := '''' 147 | abc \t 148 | def \" 149 | ''' 150 | 151 | -------------------------------------------------------------------------------- 152 | 153 | (source_file 154 | (assignment 155 | (identifier) 156 | (expression 157 | (value 158 | (string)))) 159 | (assignment 160 | (identifier) 161 | (expression 162 | (value 163 | (string)))) 164 | (assignment 165 | (identifier) 166 | (expression 167 | (value 168 | (string))))) 169 | 170 | ================================================================================ 171 | indented raw with nested delim 172 | ================================================================================ 173 | 174 | foo := ''' a ' ''' 175 | bar:= '''b 176 | 'baz' 177 | ''' 178 | 179 | -------------------------------------------------------------------------------- 180 | 181 | (source_file 182 | (assignment 183 | (identifier) 184 | (expression 185 | (value 186 | (string)))) 187 | (assignment 188 | (identifier) 189 | (expression 190 | (value 191 | (string))))) 192 | 193 | ================================================================================ 194 | backticks 195 | ================================================================================ 196 | 197 | foo := `echo hi` 198 | bar := `echo hi 199 | echo bye 200 | ` 201 | 202 | -------------------------------------------------------------------------------- 203 | 204 | (source_file 205 | (assignment 206 | (identifier) 207 | (expression 208 | (value 209 | (external_command 210 | (command_body))))) 211 | (assignment 212 | (identifier) 213 | (expression 214 | (value 215 | (external_command 216 | (command_body)))))) 217 | 218 | ================================================================================ 219 | indented backticks 220 | ================================================================================ 221 | 222 | foo := ```echo hi``` 223 | bar := ``` 224 | echo bye 225 | ``` 226 | -------------------------------------------------------------------------------- 227 | 228 | (source_file 229 | (assignment 230 | (identifier) 231 | (expression 232 | (value 233 | (external_command 234 | (command_body))))) 235 | (assignment 236 | (identifier) 237 | (expression 238 | (value 239 | (external_command 240 | (command_body)))))) 241 | 242 | ================================================================================ 243 | indented backticks with nested delim 244 | ================================================================================ 245 | 246 | foo := ``` a ` ``` 247 | bar:= ```b 248 | `baz` 249 | ``` 250 | 251 | -------------------------------------------------------------------------------- 252 | 253 | (source_file 254 | (assignment 255 | (identifier) 256 | (expression 257 | (value 258 | (external_command 259 | (command_body))))) 260 | (assignment 261 | (identifier) 262 | (expression 263 | (value 264 | (external_command 265 | (command_body)))))) 266 | -------------------------------------------------------------------------------- /test/corpus/injections.txt: -------------------------------------------------------------------------------- 1 | ================================================================================ 2 | global shebang 3 | ================================================================================ 4 | #!/usr/bin/env just 5 | 6 | -------------------------------------------------------------------------------- 7 | 8 | (source_file 9 | (shebang 10 | (language))) 11 | 12 | ================================================================================ 13 | set shell basic 14 | ================================================================================ 15 | 16 | set shell := ["fooshell"] 17 | 18 | -------------------------------------------------------------------------------- 19 | 20 | (source_file 21 | (setting 22 | (string))) 23 | 24 | ================================================================================ 25 | set shell with args 26 | ================================================================================ 27 | 28 | set shell := ['nu', '-m', 'light', '-c'] 29 | 30 | -------------------------------------------------------------------------------- 31 | 32 | (source_file 33 | (setting 34 | (string) 35 | (string) 36 | (string) 37 | (string))) 38 | 39 | ================================================================================ 40 | set shell still parse empty 41 | ================================================================================ 42 | 43 | set shell := [] 44 | 45 | -------------------------------------------------------------------------------- 46 | 47 | (source_file 48 | (setting)) 49 | -------------------------------------------------------------------------------- /test/corpus/multiline.txt: -------------------------------------------------------------------------------- 1 | ================================================================================ 2 | trailing whitespace 3 | ================================================================================ 4 | 5 | a: # 6 | -------------------------------------------------------------------------------- 7 | 8 | (source_file 9 | (recipe 10 | (recipe_header 11 | (identifier)) 12 | (comment))) 13 | 14 | ================================================================================ 15 | smooshed recipes 16 | ================================================================================ 17 | 18 | foo: 19 | echo foo 20 | bar: 21 | echo bar 22 | 23 | -------------------------------------------------------------------------------- 24 | 25 | (source_file 26 | (recipe 27 | (recipe_header 28 | (identifier)) 29 | (recipe_body 30 | (recipe_line 31 | (text)))) 32 | (recipe 33 | (recipe_header 34 | (identifier)) 35 | (recipe_body 36 | (recipe_line 37 | (text))))) 38 | 39 | ================================================================================ 40 | statement_wrap 41 | ================================================================================ 42 | 43 | a := "foo" + \ 44 | "bar" 45 | 46 | -------------------------------------------------------------------------------- 47 | 48 | (source_file 49 | (assignment 50 | (identifier) 51 | (expression 52 | (value 53 | (string)) 54 | (value 55 | (string))))) 56 | 57 | ================================================================================ 58 | dependency_wrap 59 | ================================================================================ 60 | 61 | baz: foo \ 62 | bar 63 | echo baz {{ a }} 64 | 65 | -------------------------------------------------------------------------------- 66 | 67 | (source_file 68 | (recipe 69 | (recipe_header 70 | (identifier) 71 | (dependencies 72 | (dependency 73 | (identifier)) 74 | (dependency 75 | (identifier)))) 76 | (recipe_body 77 | (recipe_line 78 | (text) 79 | (interpolation 80 | (expression 81 | (value 82 | (identifier)))))))) 83 | -------------------------------------------------------------------------------- /test/corpus/statements.txt: -------------------------------------------------------------------------------- 1 | ================================================================================ 2 | empty 3 | ================================================================================ 4 | 5 | -------------------------------------------------------------------------------- 6 | 7 | (source_file) 8 | 9 | ================================================================================ 10 | assignment 11 | ================================================================================ 12 | 13 | foo := "abc" 14 | 15 | -------------------------------------------------------------------------------- 16 | 17 | (source_file 18 | (assignment 19 | (identifier) 20 | (expression 21 | (value 22 | (string))))) 23 | 24 | ================================================================================ 25 | expression 26 | ================================================================================ 27 | 28 | foo := "abc" + 'def' 29 | 30 | -------------------------------------------------------------------------------- 31 | 32 | (source_file 33 | (assignment 34 | (identifier) 35 | (expression 36 | (value 37 | (string)) 38 | (value 39 | (string))))) 40 | 41 | ================================================================================ 42 | paths 43 | ================================================================================ 44 | 45 | relative := "abc" / 'def' 46 | absolute := / "abc" 47 | combined := "foo" / bar() / baz 48 | 49 | -------------------------------------------------------------------------------- 50 | 51 | (source_file 52 | (assignment 53 | (identifier) 54 | (expression 55 | (value 56 | (string)) 57 | (value 58 | (string)))) 59 | (assignment 60 | (identifier) 61 | (expression 62 | (value 63 | (string)))) 64 | (assignment 65 | (identifier) 66 | (expression 67 | (value 68 | (string)) 69 | (value 70 | (function_call 71 | (identifier))) 72 | (value 73 | (identifier))))) 74 | 75 | ================================================================================ 76 | if 77 | ================================================================================ 78 | 79 | foo := if bar == baz { qux } else { quux } 80 | 81 | -------------------------------------------------------------------------------- 82 | 83 | (source_file 84 | (assignment 85 | (identifier) 86 | (expression 87 | (if_expression 88 | (condition 89 | (expression 90 | (value 91 | (identifier))) 92 | (expression 93 | (value 94 | (identifier)))) 95 | (expression 96 | (value 97 | (identifier))) 98 | (else_clause 99 | (expression 100 | (value 101 | (identifier)))))))) 102 | 103 | ================================================================================ 104 | else if 105 | ================================================================================ 106 | 107 | foo := if a == "x" { 108 | "val" 109 | } else if "1" != "2" { 110 | foo_fn("val") 111 | } else if "3" == "4" { 112 | if some_bool { val } 113 | } else { val } 114 | 115 | -------------------------------------------------------------------------------- 116 | 117 | (source_file 118 | (assignment 119 | (identifier) 120 | (expression 121 | (if_expression 122 | (condition 123 | (expression 124 | (value 125 | (identifier))) 126 | (expression 127 | (value 128 | (string)))) 129 | (expression 130 | (value 131 | (string))) 132 | (else_if_clause 133 | (condition 134 | (expression 135 | (value 136 | (string))) 137 | (expression 138 | (value 139 | (string)))) 140 | (expression 141 | (value 142 | (function_call 143 | (identifier) 144 | (sequence 145 | (expression 146 | (value 147 | (string)))))))) 148 | (else_if_clause 149 | (condition 150 | (expression 151 | (value 152 | (string))) 153 | (expression 154 | (value 155 | (string)))) 156 | (expression 157 | (if_expression 158 | (condition 159 | (expression 160 | (value 161 | (identifier)))) 162 | (expression 163 | (value 164 | (identifier)))))) 165 | (else_clause 166 | (expression 167 | (value 168 | (identifier)))))))) 169 | 170 | ================================================================================ 171 | function 172 | ================================================================================ 173 | 174 | foo := some_fn("param1", param2) 175 | 176 | -------------------------------------------------------------------------------- 177 | 178 | (source_file 179 | (assignment 180 | (identifier) 181 | (expression 182 | (value 183 | (function_call 184 | (identifier) 185 | (sequence 186 | (expression 187 | (value 188 | (string))) 189 | (expression 190 | (value 191 | (identifier))))))))) 192 | 193 | ================================================================================ 194 | export 195 | ================================================================================ 196 | 197 | export foo := "abc" 198 | 199 | -------------------------------------------------------------------------------- 200 | 201 | (source_file 202 | (export 203 | (assignment 204 | (identifier) 205 | (expression 206 | (value 207 | (string)))))) 208 | 209 | ================================================================================ 210 | import 211 | ================================================================================ 212 | 213 | import 'foo/bar.just' 214 | import? 'baz.just' 215 | 216 | -------------------------------------------------------------------------------- 217 | 218 | (source_file 219 | (import 220 | (string)) 221 | (import 222 | (string))) 223 | 224 | ================================================================================ 225 | modules 226 | ================================================================================ 227 | 228 | mod foo 229 | mod bar 'PATH' 230 | mod? baz 231 | mod? qux "PATH" 232 | 233 | -------------------------------------------------------------------------------- 234 | 235 | (source_file 236 | (module 237 | (identifier)) 238 | (module 239 | (identifier) 240 | (string)) 241 | (module 242 | (identifier)) 243 | (module 244 | (identifier) 245 | (string))) 246 | 247 | ================================================================================ 248 | set 249 | ================================================================================ 250 | 251 | set SOME_BOOL 252 | set FOO := true 253 | set dotenv-load 254 | set tempdir := "/tmp/just" 255 | set shell := ["powershell.exe", "-c"] 256 | 257 | -------------------------------------------------------------------------------- 258 | 259 | (source_file 260 | (setting 261 | (identifier)) 262 | (setting 263 | (identifier) 264 | (boolean)) 265 | (setting 266 | (identifier)) 267 | (setting 268 | (identifier) 269 | (string)) 270 | (setting 271 | (string) 272 | (string))) 273 | 274 | ================================================================================ 275 | comment 276 | ================================================================================ 277 | 278 | # comment 279 | foo := bar # + baz 280 | 281 | -------------------------------------------------------------------------------- 282 | 283 | (source_file 284 | (comment) 285 | (assignment 286 | (identifier) 287 | (expression 288 | (value 289 | (identifier))) 290 | (comment))) 291 | 292 | ================================================================================ 293 | shebang 294 | ================================================================================ 295 | 296 | #!/usr/bin/env just 297 | 298 | foo: 299 | body 300 | 301 | -------------------------------------------------------------------------------- 302 | 303 | (source_file 304 | (shebang 305 | (language)) 306 | (recipe 307 | (recipe_header 308 | (identifier)) 309 | (recipe_body 310 | (recipe_line 311 | (text))))) 312 | 313 | ================================================================================ 314 | shebang with space 315 | ================================================================================ 316 | 317 | #! /usr/bin/env just 318 | 319 | foo: 320 | body 321 | 322 | -------------------------------------------------------------------------------- 323 | 324 | (source_file 325 | (shebang 326 | (language)) 327 | (recipe 328 | (recipe_header 329 | (identifier)) 330 | (recipe_body 331 | (recipe_line 332 | (text))))) 333 | 334 | ================================================================================ 335 | regex literal 336 | ================================================================================ 337 | 338 | foo_re := if ".JUSTFILE" =~ '(?i)\.?just(?:file)?' { "match" } else { "mismatch" } 339 | 340 | -------------------------------------------------------------------------------- 341 | 342 | (source_file 343 | (assignment 344 | (identifier) 345 | (expression 346 | (if_expression 347 | (condition 348 | (expression 349 | (value 350 | (string))) 351 | (regex_literal 352 | (string))) 353 | (expression 354 | (value 355 | (string))) 356 | (else_clause 357 | (expression 358 | (value 359 | (string)))))))) 360 | 361 | ================================================================================ 362 | attribute on alias: Issue #158 363 | ================================================================================ 364 | 365 | [private] 366 | alias o := open 367 | 368 | -------------------------------------------------------------------------------- 369 | 370 | (source_file 371 | (alias 372 | (attribute 373 | (identifier)) 374 | (identifier) 375 | (identifier))) 376 | 377 | -------------------------------------------------------------------------------- /test/crash-4b0422bb457cd6b39d1f8549f6739830254718a0z-assertion: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IndianBoy42/tree-sitter-just/bb0c898a80644de438e6efe5d88d30bf092935cd/test/crash-4b0422bb457cd6b39d1f8549f6739830254718a0z-assertion -------------------------------------------------------------------------------- /test/highlight/injections-global-pwsh.just: -------------------------------------------------------------------------------- 1 | set shell := ["pwsh", "-c"] 2 | set dotenv-filename := ".env-local" 3 | set dotenv-load 4 | 5 | recipe: 6 | Write-Host "Hello, world!" 7 | Get-ChildItem -Path C:\ 8 | -------------------------------------------------------------------------------- /test/highlight/injections-global-py.just: -------------------------------------------------------------------------------- 1 | set shell := ["python3", "-c"] 2 | set dotenv-filename := ".env-local" 3 | set dotenv-load 4 | 5 | recipe: 6 | if True: print(100) 7 | if "foo" != None: print("foo") 8 | -------------------------------------------------------------------------------- /test/highlight/injections.just: -------------------------------------------------------------------------------- 1 | # Injection highlighting shows up as none during tests 2 | 3 | localhost := `if [ -f "foo" ]; then; echo hi; fi` 4 | # <- variable 5 | # ^^ operator 6 | # ^ punctuation.special 7 | # ^ punctuation.special 8 | 9 | # interpolation within injection 10 | foo := `[ {{ dir(localhost) }} = "hi" ] && "bye"` 11 | # <- variable 12 | # ^^ operator 13 | # ^ punctuation.special 14 | # ^^ punctuation.bracket 15 | # ^^^ function.call 16 | # ^ punctuation.bracket 17 | # ^^^^^^^^^ variable 18 | # ^ punctuation.bracket 19 | # ^^ punctuation.bracket 20 | # ^ punctuation.special 21 | 22 | stuff := ``` 23 | echo foo 24 | echo bar 25 | ``` 26 | # <- punctuation.special 27 | 28 | # These have to be verified manually, no automated tests are available 29 | 30 | foo: 31 | echo "hello {{ foo }}" 32 | if [ -f "foo" ]; then; \ 33 | echo hi; \ 34 | fi 35 | 36 | py_shebang: 37 | #!/usr/bin/env python 38 | if foo is not None: 39 | print("bar") 40 | 41 | perl_shebang: 42 | #!/usr/bin/env perl 43 | my $iter = $dir->iterator; 44 | while (my $file = $iter->()) { 45 | 46 | # See if it is a directory and skip 47 | next if $file->is_dir(); 48 | 49 | # Print out the file name and path 50 | print "$file\n"; 51 | } 52 | -------------------------------------------------------------------------------- /test/highlight/invalid-syntax.just: -------------------------------------------------------------------------------- 1 | # Test that numbers are parsed as errors but do not mess up the syntax 2 | 3 | a := 100 4 | # <- variable 5 | # ^^ operator 6 | # ^^^ error 7 | 8 | foo: 9 | # <- function 10 | # ^ operator 11 | 12 | b := "a" + 1.234 13 | # <- variable 14 | # ^^ operator 15 | # ^^^ string 16 | # ^ operator 17 | # ^^^^ error 18 | 19 | bar: 20 | # <- function 21 | # ^ operator 22 | -------------------------------------------------------------------------------- /test/highlight/multiline.just: -------------------------------------------------------------------------------- 1 | # Wrapped expression 2 | a := "foo" + \ 3 | "bar" 4 | # ^^^^^ string 5 | 6 | # Smooshed recipes 7 | foo: 8 | echo foo {{ a }} 9 | bar: 10 | echo bar {{ os() }} 11 | # ^^ function.call 12 | 13 | # Wrapped dependencies 14 | baz: foo \ 15 | bar 16 | # ^^^ function.call 17 | -------------------------------------------------------------------------------- /test/highlight/recipes.just: -------------------------------------------------------------------------------- 1 | #!/use/bin/env just 2 | # <- keyword.directive 3 | # ^^^^^^^^^^^^^^^^^ keyword.directive 4 | 5 | foo: 6 | # <- function 7 | # ^ operator 8 | 9 | @bar: 10 | # <- operator 11 | # ^ function 12 | # ^ operator 13 | 14 | baz: foo bar 15 | # <- function 16 | # ^ operator 17 | # ^ function.call 18 | # ^ function.call 19 | 20 | qux var1: 21 | # <- function 22 | # ^^^ variable.parameter 23 | # ^ operator 24 | 25 | quux var *var2: 26 | # <- function 27 | # ^^^ variable.parameter 28 | # ^ operator 29 | # ^^^ variable.parameter 30 | # ^ operator 31 | 32 | corge +quux: baz (quux quux) 33 | # <- function 34 | # ^ operator 35 | # ^^^^ variable.parameter 36 | # ^ operator 37 | # ^^^ function.call 38 | # ^ punctuation.bracket 39 | # ^^^^ function.call 40 | # ^^^ variable 41 | # ^ punctuation.bracket 42 | 43 | grault abc="def": 44 | # <- function 45 | # ^^^ variable.parameter 46 | # ^ operator 47 | # ^^^^ string 48 | # ^ operator 49 | 50 | garply: foo && bar 51 | # <- function 52 | # ^ operator 53 | # ^^^ function.call 54 | # ^^ operator 55 | # ^^^ function.call 56 | 57 | waldo a="b": foo bar && baz 58 | # <- function 59 | # ^ variable.parameter 60 | # ^ operator 61 | # ^^^ string 62 | # ^ operator 63 | # ^^^ function.call 64 | # ^^^ function.call 65 | # ^^ operator 66 | # ^^^ function.call 67 | 68 | fred: garply && (waldo "x") 69 | # <- function 70 | # ^^^^^^ function.call 71 | # ^^ operator 72 | # ^ punctuation.bracket 73 | # ^^^^ function.call 74 | # ^^^ string 75 | # ^ punctuation.bracket 76 | 77 | # plugh 78 | plugh: 79 | echo "plugh" 80 | # xyzzy 81 | xyzzy: 82 | echo "xyzzy" 83 | 84 | # FIXME: can't test these because we can't place comments between 85 | [private] 86 | [confirm, no-cd] 87 | attributes: 88 | -------------------------------------------------------------------------------- /test/highlight/statements.just: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env just 2 | # <- keyword.directive 3 | 4 | foo := "abc" 5 | # <- variable 6 | # ^^ operator 7 | # ^^^^^ string 8 | 9 | bar := "abc" + 'def' + foo 10 | # <- variable 11 | # ^^ operator 12 | # ^^^^^ string 13 | # ^ operator 14 | # ^^^^^ string 15 | # ^ operator 16 | # ^ variable 17 | 18 | relative := "abc" / "def" 19 | # <- variable 20 | # ^^ operator 21 | # ^^^^^ string 22 | # ^ operator 23 | # ^^^^^ string 24 | 25 | 26 | absolute := / "abc" 27 | # <- variable 28 | # ^^ operator 29 | # ^ operator 30 | # ^^^^^ string 31 | 32 | combined := "foo" / os() / bar 33 | # <- variable 34 | # ^^ operator 35 | # ^^^^^ string 36 | # ^ operator 37 | # ^^ function.call 38 | # ^^ punctuation.bracket 39 | # ^ operator 40 | # ^^^ variable 41 | 42 | 43 | # We can't really test multiline strings completely since check comments only 44 | # apply to one line above, but should be okay here 45 | 46 | foo0 := "a" 47 | # <- variable 48 | # ^^ operator 49 | # ^^^ string 50 | 51 | bar0 := "b 52 | c 53 | " 54 | # <- string 55 | 56 | baz0 := "a\"\t" 57 | # <- variable 58 | # ^ operator 59 | # ^^ string 60 | # ^^^^ constant.character.escape 61 | # ^ string 62 | 63 | foo1 := """a""" 64 | # <- variable 65 | # ^^ operator 66 | # ^^^^^^^ string 67 | 68 | bar1:= """b 69 | c 70 | """ 71 | # <- string 72 | 73 | baz1 := """ 74 | abc \t 75 | def \" 76 | \"""" 77 | # <- constant.character.escape 78 | # ^^^ string 79 | 80 | foo2 := 'a' 81 | # <- variable 82 | # ^^ operator 83 | # ^^^ string 84 | 85 | bar2 := 'b 86 | c 87 | ' 88 | # <- string 89 | 90 | baz2 := 'a\"\t' 91 | # <- variable 92 | # ^^ operator 93 | # ^^^ string 94 | 95 | foo3 := '''a''' 96 | # <- variable 97 | # ^^ operator 98 | # ^^^^^^^ string 99 | 100 | bar3 := '''b 101 | c 102 | ''' 103 | # <- string 104 | 105 | baz3 := ''' 106 | abc \t 107 | def \" 108 | ''' 109 | # <- string 110 | 111 | foo4 := `echo hi` 112 | bar4 := `echo hi 113 | echo bye 114 | ` 115 | 116 | foo5 := ```echo hi``` 117 | # <- variable 118 | # ^^ operator 119 | 120 | # FIXME 121 | # $$$ punctuation.special 122 | # $$$ punctuation.special 123 | 124 | bar5 := ``` 125 | echo bye 126 | ``` 127 | # FIXME 128 | # <$ punctuation.special 129 | 130 | foo_if := if foo == "x" { "val" } else if "1" != "2" { env("val") } else { bar } 131 | # <- variable 132 | # ^^ operator 133 | # ^^ keyword.control.conditional 134 | # ^^^ variable 135 | # ^^ operator 136 | # ^^^ string 137 | # ^ punctuation.bracket 138 | # ^^^^^ string 139 | # ^ punctuation.bracket 140 | # ^^^^ keyword.control.conditional 141 | # ^^ keyword.control.conditional 142 | # ^^^ string 143 | # ^^ operator 144 | # ^^^ string 145 | # ^ punctuation.bracket 146 | # ^^^ function.call 147 | # ^ punctuation.bracket 148 | # ^^^^^ string 149 | # ^ punctuation.bracket 150 | # ^^^^ keyword.control.conditional 151 | # ^^^ variable 152 | 153 | foo_fn := env("abc", foo) 154 | # <- variable 155 | # ^^ operator 156 | # ^^^ function.call 157 | # ^ punctuation.bracket 158 | # ^^^^ string 159 | # ^ punctuation.delimiter 160 | # ^^^ variable 161 | # ^ punctuation.bracket 162 | 163 | export fooexp := "abc" 164 | # <- keyword.control.import 165 | # ^^^^^^ variable 166 | # ^^ operator 167 | # ^^^^ string 168 | 169 | import? 'baz.just' 170 | # <- keyword.control.import 171 | # ^ operator 172 | # ^^^^^^^^^^ string 173 | 174 | mod? foomod 175 | # <- keyword.module 176 | # ^ operator 177 | # ^^^^^^ namespace 178 | 179 | mod barmod 'recipes.just' 180 | # <- keyword.module 181 | # ^^^^^^ namespace 182 | # ^^^^^ string 183 | 184 | mod? qux "PATH" 185 | # <- keyword.module 186 | # ^ operator 187 | # ^^^ namespace 188 | # ^^^^^ string 189 | 190 | set dotenv-load 191 | set fallback := false 192 | set tempdir := "/tmp/just" 193 | set shell := ["powershell.exe", "-c"] 194 | 195 | # comment 196 | # <- comment.line 197 | 198 | foo_cmt := bar # + baz 199 | # <- variable 200 | # ^ comment.line 201 | 202 | # FIXME: the RHS string should parse as an injection, if we ever have a way to test that 203 | foo_re := if ".JUSTFILE" =~ '(?i)\.?just(?:file)?' { "match" } else { "mismatch" } 204 | # <- variable 205 | # ^^ operator 206 | # ^^ keyword.control.conditional 207 | # ^^^^^^^^^^^ string 208 | # ^^ operator 209 | # ^^^^^^^^^^^^^^^^^^^^^^ string 210 | # ^ punctuation.bracket 211 | # ^ punctuation.bracket 212 | # ^^^^ keyword.control.conditional 213 | # ^ punctuation.bracket 214 | # ^^^^^^^^^ string 215 | # ^ punctuation.bracket 216 | -------------------------------------------------------------------------------- /test/issue69-segfault.just: -------------------------------------------------------------------------------- 1 | alias b := build 2 | alias c := check 3 | alias dr := dry-run 4 | alias sw := switch 5 | alias t := test 6 | alias u := update 7 | alias ui := update-input 8 | 9 | rebuildArgs := "--verbose" 10 | rebuild := if os() == "macos" { "darwin-rebuild" } else { "nixos-rebuild" } 11 | 12 | default: 13 | @just --choose 14 | 15 | [private] 16 | rebuild subcmd: 17 | {{ rebuild }} {{ subcmd }} {{ rebuildArgs }} --flake . 18 | 19 | build: (rebuild "build") 20 | 21 | dry-run: (rebuild "dry-run") 22 | 23 | switch: (rebuild "switch") 24 | 25 | test: (rebuild "test") 26 | 27 | ci: 28 | nix run \ 29 | --inputs-from . \ 30 | --override-input nixpkgs nixpkgs \ 31 | github:Mic92/nix-fast-build -- \ 32 | --no-nom \ 33 | --skip-cached \ 34 | --option accept-flake-config true \ 35 | --option allow-import-from-derivation false \ 36 | --flake '.#hydraJobs' 37 | 38 | check: 39 | nix flake check \ 40 | --print-build-logs \ 41 | --show-trace \ 42 | --accept-flake-config 43 | 44 | update: 45 | nix flake update 46 | 47 | update-input input: 48 | nix flake lock \ 49 | --update-input {{ input }} \ 50 | --commit-lock-file \ 51 | --commit-lockfile-summary "flake: update {{ input }}" 52 | 53 | deploy system: 54 | nix run \ 55 | --inputs-from . \ 56 | 'nixpkgs#deploy-rs' -- \ 57 | -s '.#{{ system }}' 58 | 59 | deploy-all: 60 | nix run \ 61 | --inputs-from . \ 62 | 'nixpkgs#deploy-rs' -- -s 63 | 64 | a: 65 | 66 | clean: 67 | rm -rf \ 68 | result* \ 69 | repl-result-out* \ 70 | config.tf.json \ 71 | .terraform* 72 | -------------------------------------------------------------------------------- /test/readme.just: -------------------------------------------------------------------------------- 1 | build: 2 | cc *.c -o main 3 | 4 | # test everything 5 | test-all: build 6 | ./test --all 7 | 8 | # run a specific test 9 | test TEST: build 10 | ./test --test {{TEST}} 11 | 12 | # use Powershell instead of sh: 13 | set shell := ["powershell.exe", "-c"] 14 | 15 | hello: 16 | Write-Host "Hello, world!" 17 | 18 | # use cmd.exe instead of sh: 19 | set shell := ["cmd.exe", "/c"] 20 | 21 | list: 22 | dir 23 | 24 | recipe-name: 25 | echo 'This is a recipe!' 26 | 27 | # this is a comment 28 | another-recipe: 29 | @echo 'This is another recipe.' 30 | 31 | publish: 32 | cargo test 33 | # tests passed, time to publish! 34 | cargo publish 35 | 36 | build: 37 | cc main.c foo.c bar.c -o main 38 | 39 | test: build 40 | ./test 41 | 42 | sloc: 43 | @echo "`wc -l *.c` lines of code" 44 | 45 | test: 46 | echo 'Testing!' 47 | 48 | build: 49 | echo 'Building!' 50 | 51 | default: 52 | @just --list 53 | 54 | alias b := build 55 | 56 | build: 57 | echo 'Building!' 58 | 59 | set shell := ["zsh", "-cu"] 60 | 61 | foo: 62 | # this line will be run as `zsh -cu 'ls **/*.txt'` 63 | ls **/*.txt 64 | 65 | set NAME 66 | 67 | set NAME := true 68 | 69 | set export 70 | 71 | a := "hello" 72 | 73 | @foo b: 74 | echo $a 75 | echo $b 76 | 77 | set positional-arguments 78 | 79 | @foo bar: 80 | echo $0 81 | echo $1 82 | 83 | # use python3 to execute recipe lines and backticks 84 | set shell := ["python3", "-c"] 85 | 86 | # use print to capture result of evaluation 87 | foos := `print("foo" * 4)` 88 | 89 | foo: 90 | print("Snake snake snake snake.") 91 | print("{{foos}}") 92 | 93 | set shell := ["python3", "-c"] 94 | 95 | set shell := ["bash", "-uc"] 96 | 97 | set shell := ["zsh", "-uc"] 98 | 99 | set shell := ["fish", "-c"] 100 | 101 | # build stuff 102 | build: 103 | ./bin/build 104 | 105 | # test stuff 106 | test: 107 | ./bin/test 108 | 109 | version := "0.2.7" 110 | tardir := "awesomesauce-" + version 111 | tarball := tardir + ".tar.gz" 112 | 113 | publish: 114 | rm -f {{tarball}} 115 | mkdir {{tardir}} 116 | cp README.md *.c {{tardir}} 117 | tar zcvf {{tarball}} {{tardir}} 118 | scp {{tarball}} me@server.com:release/ 119 | rm -rf {{tarball}} {{tardir}} 120 | 121 | braces: 122 | echo 'I {{{{LOVE}} curly braces!' 123 | 124 | braces: 125 | echo '{{'I {{LOVE}} curly braces!'}}' 126 | 127 | braces: 128 | echo 'I {{ "{{" }}LOVE}} curly braces!' 129 | 130 | string-with-tab := "\t" 131 | string-with-newline := "\n" 132 | string-with-carriage-return := "\r" 133 | string-with-double-quote := "\"" 134 | string-with-slash := "\\" 135 | string-with-no-newline := "\ 136 | " 137 | 138 | single := ' 139 | hello 140 | ' 141 | 142 | double := " 143 | goodbye 144 | " 145 | 146 | escapes := '\t\n\r\"\\' 147 | 148 | # this string will evaluate to `foo\nbar\n` 149 | x := ''' 150 | foo 151 | bar 152 | ''' 153 | 154 | # this string will evaluate to `abc\n wuv\nbar\n` 155 | y := """ 156 | abc 157 | wuv 158 | xyz 159 | """ 160 | 161 | foo: 162 | -cat foo 163 | echo 'Done!' 164 | 165 | march := arch() 166 | mos := os() 167 | mos := os_family() 168 | 169 | system-info: 170 | @echo "This is an {{arch()}} machine". 171 | 172 | # - `env_var(key)` – Retrieves the environment variable with name `key`, aborting if it is not present. 173 | # 174 | # - `env_var_or_default(key, default)` – Retrieves the environment variable with name `key`, returning `default` if it is not present. 175 | # 176 | # - `invocation_directory()` - Retrieves the path of the current working directory, before `just` changed it (chdir'd) prior to executing commands. 177 | 178 | rustfmt: 179 | find {{invocation_directory()}} -name \*.rs -exec rustfmt {} \; 180 | 181 | build: 182 | cd {{invocation_directory()}}; ./some_script_that_needs_to_be_run_from_here 183 | 184 | script: 185 | ./{{justfile_directory()}}/scripts/some_script 186 | 187 | executable: 188 | @echo The executable is at: {{just_executable()}} 189 | 190 | serve: 191 | @echo "Starting server with database $DATABASE_ADDRESS on port $SERVER_PORT..." 192 | ./server --database $DATABASE_ADDRESS --port $SERVER_PORT 193 | 194 | localhost := `dumpinterfaces | cut -d: -f2 | sed 's/\/.*//' | sed 's/ //g'` 195 | 196 | serve: 197 | ./serve {{localhost}} 8080 198 | 199 | # This backtick evaluates the command `echo foo\necho bar\n`, which produces the value `foo\nbar\n`. 200 | stuff := ``` 201 | echo foo 202 | echo bar 203 | ``` 204 | 205 | foo := if "2" == "2" { "Good!" } else { "1984" } 206 | 207 | bar: 208 | @echo "{{foo}}" 209 | 210 | foo := if "hello" != "goodbye" { "xyz" } else { "abc" } 211 | 212 | bar: 213 | @echo {{foo}} 214 | 215 | foo := if env_var("RELEASE") == "true" { `get-something-from-release-database` } else { "dummy-value" } 216 | 217 | bar foo: 218 | echo {{ if foo == "bar" { "hello" } else { "goodbye" } }} 219 | 220 | os := "linux" 221 | 222 | test: build 223 | ./test --test {{os}} 224 | 225 | build: 226 | ./build {{os}} 227 | 228 | export RUST_BACKTRACE := "1" 229 | 230 | test: 231 | # will print a stack trace if it crashes 232 | cargo test 233 | 234 | test $RUST_BACKTRACE="1": 235 | # will print a stack trace if it crashes 236 | cargo test 237 | 238 | export FOO := "world" 239 | # This backtick will fail with "WORLD: unbound variable" 240 | BAR := `echo hello $WORLD` 241 | 242 | # Running `just a foo` will fail with "A: unbound variable" 243 | a $A $B=`echo $A`: 244 | echo $A $B 245 | 246 | build target: 247 | @echo 'Building {{target}}...' 248 | cd {{target}} && make 249 | 250 | default: (build "main") 251 | 252 | build target: 253 | @echo 'Building {{target}}...' 254 | cd {{target}} && make 255 | 256 | default := 'all' 257 | 258 | test target tests=default: 259 | @echo 'Testing {{target}}:{{tests}}...' 260 | ./test --tests {{tests}} {{target}} 261 | 262 | arch := "wasm" 263 | 264 | test triple=(arch + "-unknown-unknown"): 265 | ./test {{triple}} 266 | 267 | backup +FILES: 268 | scp {{FILES}} me@server.com: 269 | 270 | commit MESSAGE *FLAGS: 271 | git commit {{FLAGS}} -m "{{MESSAGE}}" 272 | 273 | test +FLAGS='-q': 274 | cargo test {{FLAGS}} 275 | 276 | search QUERY: 277 | lynx https://www.google.com/?q={{QUERY}} 278 | 279 | search QUERY: 280 | lynx 'https://www.google.com/?q={{QUERY}}' 281 | 282 | foo $bar: 283 | echo $bar 284 | 285 | a: 286 | echo 'A!' 287 | 288 | b: a 289 | echo 'B!' 290 | just c 291 | 292 | c: 293 | echo 'C!' 294 | 295 | polyglot: python js perl sh ruby 296 | 297 | python: 298 | #!/usr/bin/env python3 299 | print('Hello from python!') 300 | 301 | js: 302 | #!/usr/bin/env node 303 | console.log('Greetings from JavaScript!') 304 | 305 | perl: 306 | #!/usr/bin/env perl 307 | print "Larry Wall says Hi!\n"; 308 | 309 | sh: 310 | #!/usr/bin/env sh 311 | hello='Yo' 312 | echo "$hello from a shell script!" 313 | 314 | ruby: 315 | #!/usr/bin/env ruby 316 | puts "Hello from ruby!" 317 | 318 | foo: 319 | #!/usr/bin/env bash 320 | set -euxo pipefail 321 | hello='Yo' 322 | echo "$hello from Bash!" 323 | 324 | echo: 325 | #!/bin/sh 326 | 327 | echo "Hello!" 328 | 329 | foo: 330 | x=hello && echo $x # This works! 331 | y=bye 332 | echo $y # This doesn't, `y` is undefined here! 333 | 334 | foo: 335 | #!/usr/bin/env bash 336 | set -euxo pipefail 337 | x=hello 338 | echo $x 339 | 340 | foo: 341 | pwd # This `pwd` will print the same directory… 342 | cd bar 343 | pwd # …as this `pwd`! 344 | 345 | foo: 346 | cd bar && pwd 347 | 348 | foo: 349 | #!/usr/bin/env bash 350 | set -euxo pipefail 351 | cd bar 352 | pwd 353 | 354 | conditional: 355 | if true; then echo 'True!'; fi 356 | 357 | conditional: 358 | if true; then \ 359 | echo 'True!'; \ 360 | fi 361 | 362 | conditional: 363 | #!/usr/bin/env sh 364 | if true; then 365 | echo 'True!' 366 | fi 367 | 368 | for: 369 | for file in `ls .`; do echo $file; done 370 | 371 | for: 372 | for file in `ls .`; do \ 373 | echo $file; \ 374 | done 375 | 376 | for: 377 | #!/usr/bin/env sh 378 | for file in `ls .`; do 379 | echo $file 380 | done 381 | 382 | while: 383 | while `server-is-dead`; do ping -c 1 server; done 384 | 385 | while: 386 | while `server-is-dead`; do \ 387 | ping -c 1 server; \ 388 | done 389 | 390 | while: 391 | #!/usr/bin/env sh 392 | while `server-is-dead`; do 393 | do ping -c 1 server 394 | done 395 | 396 | test: _test-helper 397 | ./bin/test 398 | 399 | _test-helper: 400 | ./bin/super-secret-test-helper-stuff 401 | 402 | 403 | @quiet: 404 | echo hello 405 | echo goodbye 406 | @# all done! 407 | 408 | foo: 409 | #!/usr/bin/env bash 410 | echo 'Foo!' 411 | 412 | @bar: 413 | #!/usr/bin/env bash 414 | echo 'Bar!' 415 | 416 | default: 417 | @just --choose 418 | 419 | test: 420 | ./test 421 | -------------------------------------------------------------------------------- /test/timeout-1aa6bf37e914715f4aa49e6cf693f7abf81aaf8e: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IndianBoy42/tree-sitter-just/bb0c898a80644de438e6efe5d88d30bf092935cd/test/timeout-1aa6bf37e914715f4aa49e6cf693f7abf81aaf8e -------------------------------------------------------------------------------- /tree-sitter.json: -------------------------------------------------------------------------------- 1 | { 2 | "grammars": [ 3 | { 4 | "name": "just", 5 | "camelcase": "Just", 6 | "scope": "source.just", 7 | "path": ".", 8 | "file-types": [ 9 | "just", 10 | ".just", 11 | "justfile", 12 | "JUSTFILE", 13 | "Justfile", 14 | ".justfile", 15 | ".JUSTFILE", 16 | ".Justfile" 17 | ], 18 | "highlights": "queries-src/highlights.scm", 19 | "injections": "queries-src/injections.scm", 20 | "locals": "queries-src/locals.scm", 21 | "injection-regex": "^(?i)just(file)?$", 22 | "first-line-regex": "#!\\S*bin\\S*[/ ]just" 23 | } 24 | ], 25 | "metadata": { 26 | "version": "0.1.0", 27 | "license": "MIT", 28 | "description": "Justfiles grammar for tree-sitter", 29 | "authors": [ 30 | { 31 | "name": "Anshuman Medhi", 32 | "email": "amedhi@connect.ust.uk" 33 | }, 34 | { 35 | "name": "Trevor Gross", 36 | "email": "tmgross@umich.edu" 37 | } 38 | ], 39 | "links": { 40 | "repository": "https://github.com/IndianBoy42/tree-sitter-just" 41 | } 42 | }, 43 | "bindings": { 44 | "c": true, 45 | "go": true, 46 | "node": true, 47 | "python": true, 48 | "rust": true, 49 | "swift": true 50 | } 51 | } 52 | --------------------------------------------------------------------------------