├── .editorconfig ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── deploy.yml │ └── docs.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── pom.xml ├── scripts ├── TreeSitter_java.patch ├── fix-javadoc.sh ├── jextract.ps1 └── jextract.sh ├── spotbugs-exclude.xml └── src ├── main └── java │ └── io │ └── github │ └── treesitter │ └── jtreesitter │ ├── CapturesIterator.java │ ├── InputEdit.java │ ├── InputEncoding.java │ ├── Language.java │ ├── LanguageMetadata.java │ ├── Logger.java │ ├── LookaheadIterator.java │ ├── MatchesIterator.java │ ├── NativeLibraryLookup.java │ ├── Node.java │ ├── ParseCallback.java │ ├── Parser.java │ ├── Point.java │ ├── Query.java │ ├── QueryCapture.java │ ├── QueryCursor.java │ ├── QueryError.java │ ├── QueryMatch.java │ ├── QueryPredicate.java │ ├── QueryPredicateArg.java │ ├── Range.java │ ├── Tree.java │ ├── TreeCursor.java │ ├── Unsigned.java │ ├── internal │ └── ChainedLibraryLookup.java │ └── package-info.java └── test └── java └── io └── github └── treesitter └── jtreesitter ├── LanguageTest.java ├── LookaheadIteratorTest.java ├── NodeTest.java ├── ParserTest.java ├── QueryCursorTest.java ├── QueryTest.java ├── TreeCursorTest.java ├── TreeTest.java └── languages └── TreeSitterJava.java /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | max_line_length = 120 11 | 12 | [LICENSE] 13 | max_line_length = off 14 | 15 | [README.md] 16 | indent_size = 2 17 | max_line_length = off 18 | 19 | [*.{xml,yml,json}] 20 | indent_size = 2 21 | max_line_length = off 22 | 23 | [*.ps1] 24 | end_of_line = crlf 25 | 26 | [*.patch] 27 | trim_trailing_whitespace = false 28 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | 3 | scripts/jextract.ps1 eol=crlf 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: / 5 | schedule: 6 | interval: weekly 7 | day: sunday 8 | commit-message: 9 | prefix: ci 10 | labels: [dependencies] 11 | open-pull-requests-limit: 1 12 | groups: 13 | actions: 14 | patterns: ["*"] 15 | 16 | - package-ecosystem: gitsubmodule 17 | directory: / 18 | schedule: 19 | interval: weekly 20 | day: friday 21 | commit-message: 22 | prefix: build 23 | labels: [dependencies] 24 | open-pull-requests-limit: 1 25 | groups: 26 | submodules: 27 | patterns: ["*"] 28 | 29 | - package-ecosystem: maven 30 | directory: / 31 | schedule: 32 | interval: weekly 33 | day: saturday 34 | commit-message: 35 | prefix: build 36 | labels: [dependencies] 37 | open-pull-requests-limit: 1 38 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | paths: 7 | - pom.xml 8 | - spotbugs-exclude.xml 9 | - core 10 | - src/** 11 | - scripts/jextract.* 12 | - scripts/TreeSitter_java.patch 13 | - .github/workflows/ci.yml 14 | pull_request: 15 | branches: [master] 16 | paths: 17 | - pom.xml 18 | - spotbugs-exclude.xml 19 | - core 20 | - src/** 21 | - scripts/jextract.* 22 | - scripts/TreeSitter_java.patch 23 | - .github/workflows/ci.yml 24 | 25 | concurrency: 26 | cancel-in-progress: true 27 | group: ${{github.workflow}}-${{github.ref_name}} 28 | 29 | permissions: 30 | contents: write 31 | security-events: write 32 | 33 | jobs: 34 | test: 35 | name: Test package 36 | strategy: 37 | fail-fast: false 38 | matrix: 39 | target: [ubuntu-latest, windows-latest, macos-latest] 40 | runs-on: ${{matrix.target}} 41 | steps: 42 | - name: Checkout repository 43 | uses: actions/checkout@v4 44 | with: 45 | submodules: true 46 | - name: Set up Java 47 | uses: actions/setup-java@v4 48 | with: 49 | distribution: temurin 50 | java-version: 22 51 | cache: maven 52 | - name: Get tree-sitter commit 53 | shell: sh 54 | run: printf 'TREE_SITTER_REF=%s\n' "$(git rev-parse HEAD:core)" >> "$GITHUB_ENV" 55 | - name: Set up tree-sitter CLI 56 | if: runner.os == 'Windows' 57 | uses: tree-sitter/setup-action/cli@v2 58 | - name: Set up tree-sitter library 59 | uses: tree-sitter/setup-action/lib@v2 60 | with: 61 | tree-sitter-ref: ${{env.TREE_SITTER_REF}} 62 | - name: Set up tree-sitter-java 63 | shell: bash 64 | run: |- 65 | git clone --depth=1 https://github.com/tree-sitter/tree-sitter-java 66 | cd tree-sitter-java 67 | cmake -B build \ 68 | -DBUILD_SHARED_LIBS=ON \ 69 | -DCMAKE_INSTALL_LIBDIR=lib \ 70 | -DCMAKE_INSTALL_BINDIR=lib \ 71 | -DCMAKE_INSTALL_PREFIX="$RUNNER_TOOL_CACHE/tree-sitter/lib" 72 | cmake --build build && cmake --install build --config Debug 73 | - name: Set up jextract 74 | shell: bash 75 | run: |- 76 | if [[ $RUNNER_OS == Linux ]]; then 77 | JEXTRACT_URL+=_linux-x64_bin.tar.gz 78 | elif [[ $RUNNER_OS == macOS ]]; then 79 | JEXTRACT_URL+=_macos-aarch64_bin.tar.gz 80 | else 81 | JEXTRACT_URL+=_windows-x64_bin.tar.gz 82 | fi 83 | curl -LSs "$JEXTRACT_URL" | tar xzf - -C "$RUNNER_TOOL_CACHE" 84 | printf '%s/jextract-22/bin\n' "$RUNNER_TOOL_CACHE" >> "$GITHUB_PATH" 85 | env: 86 | # NOTE: keep this in sync with deploy, docs 87 | JEXTRACT_URL: https://download.java.net/java/early_access/jextract/22/6/openjdk-22-jextract+6-47 88 | - name: Run tests 89 | run: mvn --no-transfer-progress test 90 | - name: Patch SpotBugs SARIF report 91 | if: "!cancelled() && github.event_name == 'push'" 92 | run: mvn antrun:run@patch-sarif 93 | - name: Upload SpotBugs SARIF report 94 | uses: github/codeql-action/upload-sarif@v3 95 | if: "!cancelled() && github.event_name == 'push'" 96 | with: 97 | category: spotbugs 98 | sarif_file: target/reports/spotbugsSarif.json 99 | - name: Upload JUnit XML report 100 | uses: mikepenz/action-junit-report@v5 101 | if: "!cancelled()" 102 | with: 103 | annotate_only: true 104 | detailed_summary: true 105 | report_paths: target/reports/surefire/TEST-*.xml 106 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | push: 5 | tags: ["*"] 6 | 7 | jobs: 8 | deploy: 9 | name: Deploy package 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | environment: 14 | name: maven-central 15 | url: https://central.sonatype.com/artifact/io.github.tree-sitter/jtreesitter 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | with: 20 | submodules: true 21 | - name: Set up Java 22 | uses: actions/setup-java@v4 23 | with: 24 | distribution: temurin 25 | java-version: 22 26 | cache: maven 27 | server-id: central 28 | server-username: SONATYPE_USERNAME 29 | server-password: SONATYPE_TOKEN 30 | gpg-private-key: ${{secrets.GPG_KEY}} 31 | - name: Get tree-sitter commit 32 | run: printf 'TREE_SITTER_REF=%s\n' "$(git rev-parse HEAD:core)" >> "$GITHUB_ENV" 33 | - name: Set up tree-sitter 34 | uses: tree-sitter/setup-action/lib@v2 35 | with: 36 | tree-sitter-ref: ${{env.TREE_SITTER_REF}} 37 | - name: Set up tree-sitter-java 38 | run: |- 39 | git clone --depth=1 https://github.com/tree-sitter/tree-sitter-java 40 | make -Ctree-sitter-java all install PREFIX="$RUNNER_TOOL_CACHE/tree-sitter/lib" 41 | - name: Set up jextract 42 | run: |- 43 | curl -LSs '${{env.JEXTRACT_URL}}' | tar xzf - -C "$RUNNER_TOOL_CACHE" 44 | printf '%s/jextract-22/bin\n' "$RUNNER_TOOL_CACHE" >> "$GITHUB_PATH" 45 | env: 46 | # NOTE: keep this in sync with ci, docs 47 | JEXTRACT_URL: https://download.java.net/java/early_access/jextract/22/6/openjdk-22-jextract+6-47_linux-x64_bin.tar.gz 48 | - name: Deploy to Maven Central 49 | run: mvn --no-transfer-progress deploy -Dspotbugs.skip=true 50 | env: 51 | SONATYPE_USERNAME: ${{secrets.SONATYPE_USERNAME}} 52 | SONATYPE_TOKEN: ${{secrets.SONATYPE_TOKEN}} 53 | MAVEN_GPG_PASSPHRASE: ${{secrets.GPG_PASSPHRASE}} 54 | - name: Create release 55 | run: gh release create $GITHUB_REF_NAME --generate-notes 56 | env: 57 | GH_TOKEN: ${{github.token}} 58 | GH_REPO: ${{github.repository}} 59 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | 3 | on: 4 | workflow_run: 5 | workflows: [CI] 6 | types: [completed] 7 | branches: [master] 8 | push: 9 | branches: [master] 10 | paths: 11 | - scripts/fix-javadoc.sh 12 | 13 | concurrency: 14 | cancel-in-progress: true 15 | group: ${{github.workflow}}-${{github.ref_name}} 16 | 17 | jobs: 18 | test: 19 | name: Publish docs 20 | runs-on: ubuntu-latest 21 | if: >- 22 | github.event_name == 'push' || 23 | github.event.workflow_run.conclusion == 'success' 24 | permissions: 25 | contents: read 26 | id-token: write 27 | pages: write 28 | environment: 29 | name: github-pages 30 | url: ${{steps.deployment.outputs.page_url}} 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v4 34 | with: 35 | submodules: true 36 | - name: Set up Java 37 | uses: actions/setup-java@v4 38 | with: 39 | distribution: temurin 40 | java-version: 22 41 | cache: maven 42 | - name: Set up tree-sitter 43 | uses: tree-sitter/setup-action/lib@v2 44 | - name: Set up tree-sitter-java 45 | run: |- 46 | git clone --depth=1 https://github.com/tree-sitter/tree-sitter-java 47 | make -Ctree-sitter-java all install PREFIX="$RUNNER_TOOL_CACHE/tree-sitter/lib" 48 | - name: Set up jextract 49 | run: |- 50 | curl -LSs '${{env.JEXTRACT_URL}}' | tar xzf - -C "$RUNNER_TOOL_CACHE" 51 | printf '%s/jextract-22/bin\n' "$RUNNER_TOOL_CACHE" >> "$GITHUB_PATH" 52 | env: 53 | # NOTE: keep this in sync with ci, deploy 54 | JEXTRACT_URL: https://download.java.net/java/early_access/jextract/22/6/openjdk-22-jextract+6-47_linux-x64_bin.tar.gz 55 | - name: Build javadoc 56 | run: mvn --no-transfer-progress javadoc:javadoc antrun:run@fix-javadoc 57 | - name: Upload pages artifact 58 | uses: actions/upload-pages-artifact@v3 59 | with: 60 | path: target/reports/apidocs 61 | - name: Publish to GitHub Pages 62 | id: deployment 63 | uses: actions/deploy-pages@v4 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Maven ### 2 | target/ 3 | 4 | ### JetBrains ### 5 | .idea/* 6 | out/ 7 | *.iws 8 | *.iml 9 | *.ipr 10 | *.trace 11 | *.log 12 | .attach_* 13 | 14 | # Eclipse 15 | .settings/ 16 | .classpath 17 | .project 18 | 19 | ### Vim ### 20 | [._]*.s[a-v][a-z] 21 | [._]*.sw[a-p] 22 | [._]s[a-rt-v][a-z] 23 | [._]ss[a-gi-z] 24 | [._]sw[a-p] 25 | Session.vim 26 | .nvimrc 27 | tags 28 | *~ 29 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "tree-sitter"] 2 | url = https://github.com/tree-sitter/tree-sitter 3 | path = core 4 | branch = release-0.25 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 tree-sitter contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Java Tree-sitter 2 | 3 | [![CI][ci]](https://github.com/tree-sitter/java-tree-sitter/actions/workflows/ci.yml) 4 | [![central][central]](https://central.sonatype.com/artifact/io.github.tree-sitter/jtreesitter) 5 | [![docs][docs]](https://tree-sitter.github.io/java-tree-sitter/) 6 | 7 | Java bindings to the [tree-sitter] parsing library. 8 | 9 | ## Building 10 | 11 | - Install JDK 22 and set `JAVA_HOME` to it 12 | - Download [jextract] and add it to your `PATH` 13 | - Install the `tree-sitter` & `tree-sitter-java` libraries 14 | 15 | ```bash 16 | git clone https://github.com/tree-sitter/java-tree-sitter 17 | cd java-tree-sitter 18 | git submodule update --init 19 | mvn test 20 | ``` 21 | 22 | ## Alternatives 23 | 24 | These alternatives support older JDK versions or Android: 25 | 26 | - [tree-sitter/kotlin-tree-sitter](https://github.com/tree-sitter/kotlin-tree-sitter) (JDK 17+, Android SDK 23+, Kotlin 1.9) 27 | - [bonede/tree-sitter-ng](https://github.com/bonede/tree-sitter-ng) (JDK 8+) 28 | - [seart-group/java-tree-sitter](https://github.com/seart-group/java-tree-sitter) (JDK 11+) 29 | - [AndroidIDEOfficial/android-tree-sitter](https://github.com/AndroidIDEOfficial/android-tree-sitter) (Android SDK 21+) 30 | 31 | [tree-sitter]: https://tree-sitter.github.io/tree-sitter/ 32 | [ci]: https://img.shields.io/github/actions/workflow/status/tree-sitter/java-tree-sitter/ci.yml?logo=github&label=CI 33 | [central]: https://img.shields.io/maven-central/v/io.github.tree-sitter/jtreesitter?logo=sonatype&label=Maven%20Central 34 | [docs]: https://img.shields.io/github/deployments/tree-sitter/java-tree-sitter/github-pages?logo=githubpages&label=API%20Docs 35 | [FFM]: https://docs.oracle.com/en/java/javase/22/core/foreign-function-and-memory-api.html 36 | [jextract]: https://jdk.java.net/jextract/ 37 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 4.0.0 7 | io.github.tree-sitter 8 | jtreesitter 9 | JTreeSitter 10 | 0.25.1 11 | Java bindings to the tree-sitter parsing library 12 | https://tree-sitter.github.io/java-tree-sitter/ 13 | 2024 14 | 15 | tree-sitter 16 | https://github.com/tree-sitter 17 | 18 | 19 | 20 | MIT 21 | https://spdx.org/licenses/MIT.html 22 | 23 | 24 | 25 | 26 | ObserverOfTime 27 | ObserverOfTime 28 | chronobserver@disroot.org 29 | https://github.com/ObserverOfTime 30 | 31 | 32 | 33 | https://github.com/tree-sitter/java-tree-sitter 34 | scm:git:git://github.com/tree-sitter/java-tree-sitter.git 35 | scm:git:ssh://github.com/tree-sitter/java-tree-sitter.git 36 | 37 | 38 | Github Actions 39 | https://github.com/tree-sitter/java-tree-sitter/actions 40 | 41 | 42 | GitHub Issues 43 | https://github.com/tree-sitter/java-tree-sitter/issues 44 | 45 | 46 | 22 47 | 22 48 | UTF-8 49 | true 50 | true 51 | false 52 | false 53 | true 54 | 55 | 56 | 57 | org.jspecify 58 | jspecify 59 | 1.0.0 60 | 61 | 62 | org.junit.jupiter 63 | junit-jupiter-api 64 | test 65 | 66 | 67 | 68 | 69 | 70 | maven-antrun-plugin 71 | 3.1.0 72 | 73 | 74 | jextract 75 | generate-sources 76 | 77 | run 78 | 79 | 80 | ${jextract.skip} 81 | 82 | Generating sources using jextract 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | fix-javadoc 108 | 109 | run 110 | 111 | 112 | 113 | Adding favicon & prism.js to javadoc 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | patch-sarif 122 | 123 | run 124 | 125 | 126 | 127 | Patching SpotBugs sarif report 128 | 129 | "uri":" 130 | "uri":"src/main/java/ 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | org.codehaus.mojo 139 | build-helper-maven-plugin 140 | 3.6.0 141 | 142 | 143 | generate-sources 144 | 145 | add-source 146 | 147 | 148 | 149 | ${project.build.directory}/generated-sources/jextract 150 | 151 | 152 | 153 | 154 | 155 | 156 | com.diffplug.spotless 157 | spotless-maven-plugin 158 | 2.44.5 159 | 160 | 161 | format-sources 162 | process-sources 163 | 164 | apply 165 | 166 | 167 | 168 | format-test-sources 169 | process-test-sources 170 | 171 | apply 172 | 173 | 174 | 175 | 176 | 177 | 178 | 2.50.0 179 | 180 | false 181 | 182 | 183 | 184 | 185 | 186 | com.github.spotbugs 187 | spotbugs-maven-plugin 188 | 4.9.3.0 189 | 190 | 191 | process-classes 192 | 193 | check 194 | 195 | 196 | 197 | 198 | true 199 | false 200 | ${project.build.directory}/reports 201 | ${project.build.directory}/reports 202 | ${project.basedir}/spotbugs-exclude.xml 203 | 204 | 205 | 206 | maven-surefire-plugin 207 | 3.5.3 208 | 209 | 210 | ${project.build.directory}/reports/surefire 211 | 212 | --enable-native-access=ALL-UNNAMED 213 | 214 | 215 | 216 | maven-javadoc-plugin 217 | 3.11.2 218 | 219 | 220 | 221 | jar 222 | 223 | 224 | 225 | 226 | public 227 | true 228 | true 229 | all,-missing 230 | 231 | 232 | apiNote 233 | a 234 | API Note: 235 | 236 | 237 | implNote 238 | a 239 | Implementation Note: 240 | 241 | 242 | 243 | https://jspecify.dev/docs/api/ 244 | 245 | io.github.treesitter.jtreesitter.internal 246 |
248 | ]]>
249 |
250 |
251 | 252 | maven-source-plugin 253 | 3.3.1 254 | 255 | 256 | 257 | jar-no-fork 258 | 259 | 260 | 261 | 262 | 263 | maven-gpg-plugin 264 | 3.2.7 265 | 266 | 267 | verify 268 | 269 | sign 270 | 271 | 272 | true 273 | 274 | --no-tty 275 | --pinentry-mode 276 | loopback 277 | 278 | 279 | 280 | 281 | 282 | 283 | org.sonatype.central 284 | central-publishing-maven-plugin 285 | 0.5.0 286 | 287 | 288 | deploy 289 | 290 | publish 291 | 292 | 293 | validated 294 | ${publish.auto} 295 | ${publish.skip} 296 | ${project.artifactId}-${project.version}.zip 297 | ${project.artifactId}-${project.version}.zip 298 | 299 | 300 | 301 | true 302 | 303 |
304 |
305 | 306 | 307 | ci 308 | 309 | 310 | env.CI 311 | true 312 | 313 | 314 | 315 | false 316 | true 317 | false 318 | 319 | 320 | 321 | 322 | 323 | 324 | org.junit 325 | junit-bom 326 | 5.13.1 327 | pom 328 | import 329 | 330 | 331 | 332 |
333 | -------------------------------------------------------------------------------- /scripts/TreeSitter_java.patch: -------------------------------------------------------------------------------- 1 | --- a/generated-sources/jextract/io/github/treesitter/jtreesitter/internal/TreeSitter.java 2 | +++ b/generated-sources/jextract/io/github/treesitter/jtreesitter/internal/TreeSitter.java 3 | @@ -55,9 +55,7 @@ 4 | }; 5 | } 6 | 7 | - static final SymbolLookup SYMBOL_LOOKUP = SymbolLookup.libraryLookup(System.mapLibraryName("tree-sitter"), LIBRARY_ARENA) 8 | - .or(SymbolLookup.loaderLookup()) 9 | - .or(Linker.nativeLinker().defaultLookup()); 10 | + static final SymbolLookup SYMBOL_LOOKUP = ChainedLibraryLookup.INSTANCE.get(LIBRARY_ARENA); 11 | 12 | public static final ValueLayout.OfBoolean C_BOOL = ValueLayout.JAVA_BOOLEAN; 13 | public static final ValueLayout.OfByte C_CHAR = ValueLayout.JAVA_BYTE; 14 | @@ -8599,4 +8606,8 @@ public class TreeSitter { 15 | throw new AssertionError("should not reach here", ex$); 16 | } 17 | } 18 | + 19 | + static { 20 | + ts_set_allocator(malloc$address(), calloc$address(), realloc$address(), free$address()); 21 | + } 22 | } 23 | -------------------------------------------------------------------------------- /scripts/fix-javadoc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | shopt -s globstar 4 | 5 | style='pre[class*="language-"]::after, pre[class*="language-"]::before { box-shadow: unset; }' 6 | style+=' pre[class*="language-"] > code { z-index: unset; }' 7 | 8 | additions=( 9 | '' 10 | '' 11 | '' 13 | '' 15 | '' 17 | '' 19 | '' 21 | '' 23 | '' 25 | "" 26 | ) 27 | 28 | for f in "${1:-target}"/reports/apidocs/**/*.html; do 29 | for line in "${additions[@]}"; do 30 | sed -i "/<\/head>/i $line" "$f" 31 | done 32 | done 33 | -------------------------------------------------------------------------------- /scripts/jextract.ps1: -------------------------------------------------------------------------------- 1 | $package = 'io.github.treesitter.jtreesitter.internal' 2 | $output = "$($args[1])/generated-sources/jextract" 3 | $lib = "$($args[0])/core/lib" 4 | 5 | & jextract.ps1 ` 6 | --include-struct TSInput ` 7 | --include-struct TSInputEdit ` 8 | --include-struct TSLogger ` 9 | --include-struct TSNode ` 10 | --include-struct TSParseOptions ` 11 | --include-struct TSParseState ` 12 | --include-struct TSPoint ` 13 | --include-struct TSQueryCapture ` 14 | --include-struct TSQueryCursorOptions ` 15 | --include-struct TSQueryCursorState ` 16 | --include-struct TSQueryMatch ` 17 | --include-struct TSQueryPredicateStep ` 18 | --include-struct TSQueryPredicateStepType ` 19 | --include-struct TSLanguageMetadata ` 20 | --include-struct TSRange ` 21 | --include-struct TSTreeCursor ` 22 | --include-function free ` 23 | --include-function malloc ` 24 | --include-function calloc ` 25 | --include-function realloc ` 26 | --include-function ts_set_allocator ` 27 | --include-function ts_language_abi_version ` 28 | --include-function ts_language_copy ` 29 | --include-function ts_language_delete ` 30 | --include-function ts_language_field_count ` 31 | --include-function ts_language_field_id_for_name ` 32 | --include-function ts_language_field_name_for_id ` 33 | --include-function ts_language_metadata ` 34 | --include-function ts_language_name ` 35 | --include-function ts_language_next_state ` 36 | --include-function ts_language_state_count ` 37 | --include-function ts_language_subtypes ` 38 | --include-function ts_language_supertypes ` 39 | --include-function ts_language_symbol_count ` 40 | --include-function ts_language_symbol_for_name ` 41 | --include-function ts_language_symbol_name ` 42 | --include-function ts_language_symbol_type ` 43 | --include-function ts_lookahead_iterator_current_symbol ` 44 | --include-function ts_lookahead_iterator_current_symbol_name ` 45 | --include-function ts_lookahead_iterator_delete ` 46 | --include-function ts_lookahead_iterator_language ` 47 | --include-function ts_lookahead_iterator_new ` 48 | --include-function ts_lookahead_iterator_next ` 49 | --include-function ts_lookahead_iterator_reset ` 50 | --include-function ts_lookahead_iterator_reset_state ` 51 | --include-function ts_node_child ` 52 | --include-function ts_node_child_by_field_id ` 53 | --include-function ts_node_child_by_field_name ` 54 | --include-function ts_node_child_with_descendant ` 55 | --include-function ts_node_child_count ` 56 | --include-function ts_node_descendant_count ` 57 | --include-function ts_node_descendant_for_byte_range ` 58 | --include-function ts_node_descendant_for_point_range ` 59 | --include-function ts_node_edit ` 60 | --include-function ts_node_end_byte ` 61 | --include-function ts_node_end_point ` 62 | --include-function ts_node_eq ` 63 | --include-function ts_node_field_name_for_child ` 64 | --include-function ts_node_field_name_for_named_child ` 65 | --include-function ts_node_first_child_for_byte ` 66 | --include-function ts_node_first_named_child_for_byte ` 67 | --include-function ts_node_grammar_symbol ` 68 | --include-function ts_node_grammar_type ` 69 | --include-function ts_node_has_changes ` 70 | --include-function ts_node_has_error ` 71 | --include-function ts_node_is_error ` 72 | --include-function ts_node_is_extra ` 73 | --include-function ts_node_is_missing ` 74 | --include-function ts_node_is_named ` 75 | --include-function ts_node_is_null ` 76 | --include-function ts_node_language ` 77 | --include-function ts_node_named_child ` 78 | --include-function ts_node_named_child_count ` 79 | --include-function ts_node_named_descendant_for_byte_range ` 80 | --include-function ts_node_named_descendant_for_point_range ` 81 | --include-function ts_node_next_named_sibling ` 82 | --include-function ts_node_next_parse_state ` 83 | --include-function ts_node_next_sibling ` 84 | --include-function ts_node_parent ` 85 | --include-function ts_node_parse_state ` 86 | --include-function ts_node_prev_named_sibling ` 87 | --include-function ts_node_prev_sibling ` 88 | --include-function ts_node_start_byte ` 89 | --include-function ts_node_start_point ` 90 | --include-function ts_node_string ` 91 | --include-function ts_node_symbol ` 92 | --include-function ts_node_type ` 93 | --include-function ts_parser_cancellation_flag ` 94 | --include-function ts_parser_delete ` 95 | --include-function ts_parser_included_ranges ` 96 | --include-function ts_parser_language ` 97 | --include-function ts_parser_logger ` 98 | --include-function ts_parser_new ` 99 | --include-function ts_parser_parse ` 100 | --include-function ts_parser_parse_string ` 101 | --include-function ts_parser_parse_string_encoding ` 102 | --include-function ts_parser_parse_with_options ` 103 | --include-function ts_parser_print_dot_graphs ` 104 | --include-function ts_parser_reset ` 105 | --include-function ts_parser_set_cancellation_flag ` 106 | --include-function ts_parser_set_included_ranges ` 107 | --include-function ts_parser_set_language ` 108 | --include-function ts_parser_set_logger ` 109 | --include-function ts_parser_set_timeout_micros ` 110 | --include-function ts_parser_timeout_micros ` 111 | --include-function ts_query_capture_count ` 112 | --include-function ts_query_capture_name_for_id ` 113 | --include-function ts_query_capture_quantifier_for_id ` 114 | --include-function ts_query_cursor_delete ` 115 | --include-function ts_query_cursor_did_exceed_match_limit ` 116 | --include-function ts_query_cursor_exec ` 117 | --include-function ts_query_cursor_exec_with_options ` 118 | --include-function ts_query_cursor_match_limit ` 119 | --include-function ts_query_cursor_new ` 120 | --include-function ts_query_cursor_next_capture ` 121 | --include-function ts_query_cursor_next_match ` 122 | --include-function ts_query_cursor_remove_match ` 123 | --include-function ts_query_cursor_set_byte_range ` 124 | --include-function ts_query_cursor_set_match_limit ` 125 | --include-function ts_query_cursor_set_max_start_depth ` 126 | --include-function ts_query_cursor_set_point_range ` 127 | --include-function ts_query_cursor_set_timeout_micros ` 128 | --include-function ts_query_cursor_timeout_micros ` 129 | --include-function ts_query_delete ` 130 | --include-function ts_query_disable_capture ` 131 | --include-function ts_query_disable_pattern ` 132 | --include-function ts_query_end_byte_for_pattern ` 133 | --include-function ts_query_is_pattern_guaranteed_at_step ` 134 | --include-function ts_query_is_pattern_non_local ` 135 | --include-function ts_query_is_pattern_rooted ` 136 | --include-function ts_query_new ` 137 | --include-function ts_query_pattern_count ` 138 | --include-function ts_query_predicates_for_pattern ` 139 | --include-function ts_query_start_byte_for_pattern ` 140 | --include-function ts_query_string_count ` 141 | --include-function ts_query_string_value_for_id ` 142 | --include-function ts_tree_copy ` 143 | --include-function ts_tree_cursor_copy ` 144 | --include-function ts_tree_cursor_current_depth ` 145 | --include-function ts_tree_cursor_current_descendant_index ` 146 | --include-function ts_tree_cursor_current_field_id ` 147 | --include-function ts_tree_cursor_current_field_name ` 148 | --include-function ts_tree_cursor_current_node ` 149 | --include-function ts_tree_cursor_delete ` 150 | --include-function ts_tree_cursor_goto_descendant ` 151 | --include-function ts_tree_cursor_goto_first_child ` 152 | --include-function ts_tree_cursor_goto_first_child_for_byte ` 153 | --include-function ts_tree_cursor_goto_first_child_for_point ` 154 | --include-function ts_tree_cursor_goto_last_child ` 155 | --include-function ts_tree_cursor_goto_next_sibling ` 156 | --include-function ts_tree_cursor_goto_parent ` 157 | --include-function ts_tree_cursor_goto_previous_sibling ` 158 | --include-function ts_tree_cursor_new ` 159 | --include-function ts_tree_cursor_reset ` 160 | --include-function ts_tree_cursor_reset_to ` 161 | --include-function ts_tree_delete ` 162 | --include-function ts_tree_edit ` 163 | --include-function ts_tree_get_changed_ranges ` 164 | --include-function ts_tree_included_ranges ` 165 | --include-function ts_tree_language ` 166 | --include-function ts_tree_print_dot_graph ` 167 | --include-function ts_tree_root_node ` 168 | --include-function ts_tree_root_node_with_offset ` 169 | --include-constant TREE_SITTER_LANGUAGE_VERSION ` 170 | --include-constant TREE_SITTER_MIN_COMPATIBLE_LANGUAGE_VERSION ` 171 | --include-constant TSInputEncodingCustom ` 172 | --include-constant TSInputEncodingUTF16BE ` 173 | --include-constant TSInputEncodingUTF16LE ` 174 | --include-constant TSInputEncodingUTF8 ` 175 | --include-constant TSLogTypeLex ` 176 | --include-constant TSLogTypeParse ` 177 | --include-constant TSQuantifierOne ` 178 | --include-constant TSQuantifierOneOrMore ` 179 | --include-constant TSQuantifierZero ` 180 | --include-constant TSQuantifierZeroOrMore ` 181 | --include-constant TSQuantifierZeroOrOne ` 182 | --include-constant TSQueryErrorCapture ` 183 | --include-constant TSQueryErrorField ` 184 | --include-constant TSQueryErrorLanguage ` 185 | --include-constant TSQueryErrorNodeType ` 186 | --include-constant TSQueryErrorNone ` 187 | --include-constant TSQueryErrorStructure ` 188 | --include-constant TSQueryErrorSyntax ` 189 | --include-constant TSQueryPredicateStepTypeCapture ` 190 | --include-constant TSQueryPredicateStepTypeDone ` 191 | --include-constant TSQueryPredicateStepTypeString ` 192 | --include-constant TSSymbolTypeAnonymous ` 193 | --include-constant TSSymbolTypeAuxiliary ` 194 | --include-constant TSSymbolTypeRegular ` 195 | --include-constant TSSymbolTypeSupertype ` 196 | --include-typedef DecodeFunction ` 197 | --header-class-name TreeSitter ` 198 | --output $output ` 199 | -t $package ` 200 | -l tree-sitter ` 201 | -I "$lib/src" ` 202 | -I "$lib/include" ` 203 | -DTREE_SITTER_HIDE_SYMBOLS ` 204 | "$lib/include/tree_sitter/api.h" 205 | -------------------------------------------------------------------------------- /scripts/jextract.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | package=io.github.treesitter.jtreesitter.internal 4 | output="$2/generated-sources/jextract" 5 | lib="$1/core/lib" 6 | 7 | exec jextract \ 8 | --include-struct TSInput \ 9 | --include-struct TSInputEdit \ 10 | --include-struct TSLogger \ 11 | --include-struct TSNode \ 12 | --include-struct TSParseOptions \ 13 | --include-struct TSParseState \ 14 | --include-struct TSPoint \ 15 | --include-struct TSQueryCapture \ 16 | --include-struct TSQueryCursorOptions \ 17 | --include-struct TSQueryCursorState \ 18 | --include-struct TSQueryMatch \ 19 | --include-struct TSQueryPredicateStep \ 20 | --include-struct TSQueryPredicateStepType \ 21 | --include-struct TSLanguageMetadata \ 22 | --include-struct TSRange \ 23 | --include-struct TSTreeCursor \ 24 | --include-function free \ 25 | --include-function malloc \ 26 | --include-function calloc \ 27 | --include-function realloc \ 28 | --include-function ts_set_allocator \ 29 | --include-function ts_language_abi_version \ 30 | --include-function ts_language_copy \ 31 | --include-function ts_language_delete \ 32 | --include-function ts_language_field_count \ 33 | --include-function ts_language_field_id_for_name \ 34 | --include-function ts_language_field_name_for_id \ 35 | --include-function ts_language_metadata \ 36 | --include-function ts_language_name \ 37 | --include-function ts_language_next_state \ 38 | --include-function ts_language_state_count \ 39 | --include-function ts_language_subtypes \ 40 | --include-function ts_language_supertypes \ 41 | --include-function ts_language_symbol_count \ 42 | --include-function ts_language_symbol_for_name \ 43 | --include-function ts_language_symbol_name \ 44 | --include-function ts_language_symbol_type \ 45 | --include-function ts_lookahead_iterator_current_symbol \ 46 | --include-function ts_lookahead_iterator_current_symbol_name \ 47 | --include-function ts_lookahead_iterator_delete \ 48 | --include-function ts_lookahead_iterator_language \ 49 | --include-function ts_lookahead_iterator_new \ 50 | --include-function ts_lookahead_iterator_next \ 51 | --include-function ts_lookahead_iterator_reset \ 52 | --include-function ts_lookahead_iterator_reset_state \ 53 | --include-function ts_node_child \ 54 | --include-function ts_node_child_by_field_id \ 55 | --include-function ts_node_child_by_field_name \ 56 | --include-function ts_node_child_with_descendant \ 57 | --include-function ts_node_child_count \ 58 | --include-function ts_node_descendant_count \ 59 | --include-function ts_node_descendant_for_byte_range \ 60 | --include-function ts_node_descendant_for_point_range \ 61 | --include-function ts_node_edit \ 62 | --include-function ts_node_end_byte \ 63 | --include-function ts_node_end_point \ 64 | --include-function ts_node_eq \ 65 | --include-function ts_node_field_name_for_child \ 66 | --include-function ts_node_field_name_for_named_child \ 67 | --include-function ts_node_first_child_for_byte \ 68 | --include-function ts_node_first_named_child_for_byte \ 69 | --include-function ts_node_grammar_symbol \ 70 | --include-function ts_node_grammar_type \ 71 | --include-function ts_node_has_changes \ 72 | --include-function ts_node_has_error \ 73 | --include-function ts_node_is_error \ 74 | --include-function ts_node_is_extra \ 75 | --include-function ts_node_is_missing \ 76 | --include-function ts_node_is_named \ 77 | --include-function ts_node_is_null \ 78 | --include-function ts_node_language \ 79 | --include-function ts_node_named_child \ 80 | --include-function ts_node_named_child_count \ 81 | --include-function ts_node_named_descendant_for_byte_range \ 82 | --include-function ts_node_named_descendant_for_point_range \ 83 | --include-function ts_node_next_named_sibling \ 84 | --include-function ts_node_next_parse_state \ 85 | --include-function ts_node_next_sibling \ 86 | --include-function ts_node_parent \ 87 | --include-function ts_node_parse_state \ 88 | --include-function ts_node_prev_named_sibling \ 89 | --include-function ts_node_prev_sibling \ 90 | --include-function ts_node_start_byte \ 91 | --include-function ts_node_start_point \ 92 | --include-function ts_node_string \ 93 | --include-function ts_node_symbol \ 94 | --include-function ts_node_type \ 95 | --include-function ts_parser_cancellation_flag \ 96 | --include-function ts_parser_delete \ 97 | --include-function ts_parser_included_ranges \ 98 | --include-function ts_parser_language \ 99 | --include-function ts_parser_logger \ 100 | --include-function ts_parser_new \ 101 | --include-function ts_parser_parse \ 102 | --include-function ts_parser_parse_string \ 103 | --include-function ts_parser_parse_string_encoding \ 104 | --include-function ts_parser_parse_with_options \ 105 | --include-function ts_parser_print_dot_graphs \ 106 | --include-function ts_parser_reset \ 107 | --include-function ts_parser_set_cancellation_flag \ 108 | --include-function ts_parser_set_included_ranges \ 109 | --include-function ts_parser_set_language \ 110 | --include-function ts_parser_set_logger \ 111 | --include-function ts_parser_set_timeout_micros \ 112 | --include-function ts_parser_timeout_micros \ 113 | --include-function ts_query_capture_count \ 114 | --include-function ts_query_capture_name_for_id \ 115 | --include-function ts_query_capture_quantifier_for_id \ 116 | --include-function ts_query_cursor_delete \ 117 | --include-function ts_query_cursor_did_exceed_match_limit \ 118 | --include-function ts_query_cursor_exec \ 119 | --include-function ts_query_cursor_exec_with_options \ 120 | --include-function ts_query_cursor_match_limit \ 121 | --include-function ts_query_cursor_new \ 122 | --include-function ts_query_cursor_next_capture \ 123 | --include-function ts_query_cursor_next_match \ 124 | --include-function ts_query_cursor_remove_match \ 125 | --include-function ts_query_cursor_set_byte_range \ 126 | --include-function ts_query_cursor_set_match_limit \ 127 | --include-function ts_query_cursor_set_max_start_depth \ 128 | --include-function ts_query_cursor_set_point_range \ 129 | --include-function ts_query_cursor_set_timeout_micros \ 130 | --include-function ts_query_cursor_timeout_micros \ 131 | --include-function ts_query_delete \ 132 | --include-function ts_query_disable_capture \ 133 | --include-function ts_query_disable_pattern \ 134 | --include-function ts_query_end_byte_for_pattern \ 135 | --include-function ts_query_is_pattern_guaranteed_at_step \ 136 | --include-function ts_query_is_pattern_non_local \ 137 | --include-function ts_query_is_pattern_rooted \ 138 | --include-function ts_query_new \ 139 | --include-function ts_query_pattern_count \ 140 | --include-function ts_query_predicates_for_pattern \ 141 | --include-function ts_query_start_byte_for_pattern \ 142 | --include-function ts_query_string_count \ 143 | --include-function ts_query_string_value_for_id \ 144 | --include-function ts_tree_copy \ 145 | --include-function ts_tree_cursor_copy \ 146 | --include-function ts_tree_cursor_current_depth \ 147 | --include-function ts_tree_cursor_current_descendant_index \ 148 | --include-function ts_tree_cursor_current_field_id \ 149 | --include-function ts_tree_cursor_current_field_name \ 150 | --include-function ts_tree_cursor_current_node \ 151 | --include-function ts_tree_cursor_delete \ 152 | --include-function ts_tree_cursor_goto_descendant \ 153 | --include-function ts_tree_cursor_goto_first_child \ 154 | --include-function ts_tree_cursor_goto_first_child_for_byte \ 155 | --include-function ts_tree_cursor_goto_first_child_for_point \ 156 | --include-function ts_tree_cursor_goto_last_child \ 157 | --include-function ts_tree_cursor_goto_next_sibling \ 158 | --include-function ts_tree_cursor_goto_parent \ 159 | --include-function ts_tree_cursor_goto_previous_sibling \ 160 | --include-function ts_tree_cursor_new \ 161 | --include-function ts_tree_cursor_reset \ 162 | --include-function ts_tree_cursor_reset_to \ 163 | --include-function ts_tree_delete \ 164 | --include-function ts_tree_edit \ 165 | --include-function ts_tree_get_changed_ranges \ 166 | --include-function ts_tree_included_ranges \ 167 | --include-function ts_tree_language \ 168 | --include-function ts_tree_print_dot_graph \ 169 | --include-function ts_tree_root_node \ 170 | --include-function ts_tree_root_node_with_offset \ 171 | --include-constant TREE_SITTER_LANGUAGE_VERSION \ 172 | --include-constant TREE_SITTER_MIN_COMPATIBLE_LANGUAGE_VERSION \ 173 | --include-constant TSInputEncodingCustom \ 174 | --include-constant TSInputEncodingUTF16BE \ 175 | --include-constant TSInputEncodingUTF16LE \ 176 | --include-constant TSInputEncodingUTF8 \ 177 | --include-constant TSLogTypeLex \ 178 | --include-constant TSLogTypeParse \ 179 | --include-constant TSQuantifierOne \ 180 | --include-constant TSQuantifierOneOrMore \ 181 | --include-constant TSQuantifierZero \ 182 | --include-constant TSQuantifierZeroOrMore \ 183 | --include-constant TSQuantifierZeroOrOne \ 184 | --include-constant TSQueryErrorCapture \ 185 | --include-constant TSQueryErrorField \ 186 | --include-constant TSQueryErrorLanguage \ 187 | --include-constant TSQueryErrorNodeType \ 188 | --include-constant TSQueryErrorNone \ 189 | --include-constant TSQueryErrorStructure \ 190 | --include-constant TSQueryErrorSyntax \ 191 | --include-constant TSQueryPredicateStepTypeCapture \ 192 | --include-constant TSQueryPredicateStepTypeDone \ 193 | --include-constant TSQueryPredicateStepTypeString \ 194 | --include-constant TSSymbolTypeAnonymous \ 195 | --include-constant TSSymbolTypeAuxiliary \ 196 | --include-constant TSSymbolTypeRegular \ 197 | --include-constant TSSymbolTypeSupertype \ 198 | --include-typedef DecodeFunction \ 199 | --header-class-name TreeSitter \ 200 | --output "$output" \ 201 | -t "$package" \ 202 | -l tree-sitter \ 203 | -I "$lib/src" \ 204 | -I "$lib/include" \ 205 | -DTREE_SITTER_HIDE_SYMBOLS \ 206 | "$lib/include/tree_sitter/api.h" 207 | -------------------------------------------------------------------------------- /spotbugs-exclude.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/main/java/io/github/treesitter/jtreesitter/CapturesIterator.java: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.jtreesitter; 2 | 3 | import static io.github.treesitter.jtreesitter.internal.TreeSitter.C_INT; 4 | import static io.github.treesitter.jtreesitter.internal.TreeSitter.ts_query_cursor_next_capture; 5 | 6 | import io.github.treesitter.jtreesitter.internal.TSQueryMatch; 7 | import java.lang.foreign.MemorySegment; 8 | import java.lang.foreign.SegmentAllocator; 9 | import java.util.AbstractMap.SimpleImmutableEntry; 10 | import java.util.Spliterator; 11 | import java.util.Spliterators; 12 | import java.util.function.BiPredicate; 13 | import java.util.function.Consumer; 14 | import org.jspecify.annotations.NullMarked; 15 | import org.jspecify.annotations.Nullable; 16 | 17 | @NullMarked 18 | class CapturesIterator extends Spliterators.AbstractSpliterator> { 19 | private final @Nullable BiPredicate predicate; 20 | private final Tree tree; 21 | private final SegmentAllocator allocator; 22 | private final Query query; 23 | private final MemorySegment cursor; 24 | 25 | public CapturesIterator( 26 | Query query, 27 | MemorySegment cursor, 28 | Tree tree, 29 | SegmentAllocator allocator, 30 | @Nullable BiPredicate predicate) { 31 | super(Long.MAX_VALUE, Spliterator.IMMUTABLE | Spliterator.NONNULL); 32 | this.predicate = predicate; 33 | this.tree = tree; 34 | this.allocator = allocator; 35 | this.query = query; 36 | this.cursor = cursor; 37 | } 38 | 39 | @Override 40 | public boolean tryAdvance(Consumer> action) { 41 | var hasNoText = tree.getText() == null; 42 | MemorySegment match = allocator.allocate(TSQueryMatch.layout()); 43 | MemorySegment index = allocator.allocate(C_INT); 44 | var captureNames = query.getCaptureNames(); 45 | while (ts_query_cursor_next_capture(cursor, match, index)) { 46 | var result = QueryMatch.from(match, captureNames, tree, allocator); 47 | if (hasNoText || query.matches(predicate, result)) { 48 | var entry = new SimpleImmutableEntry<>(index.get(C_INT, 0), result); 49 | action.accept(entry); 50 | return true; 51 | } 52 | } 53 | return false; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/io/github/treesitter/jtreesitter/InputEdit.java: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.jtreesitter; 2 | 3 | import io.github.treesitter.jtreesitter.internal.TSInputEdit; 4 | import java.lang.foreign.MemorySegment; 5 | import java.lang.foreign.SegmentAllocator; 6 | import org.jspecify.annotations.NullMarked; 7 | 8 | /** An edit to a text document. */ 9 | @NullMarked 10 | public record InputEdit( 11 | @Unsigned int startByte, 12 | @Unsigned int oldEndByte, 13 | @Unsigned int newEndByte, 14 | Point startPoint, 15 | Point oldEndPoint, 16 | Point newEndPoint) { 17 | 18 | MemorySegment into(SegmentAllocator allocator) { 19 | var inputEdit = TSInputEdit.allocate(allocator); 20 | TSInputEdit.start_byte(inputEdit, startByte); 21 | TSInputEdit.old_end_byte(inputEdit, oldEndByte); 22 | TSInputEdit.new_end_byte(inputEdit, newEndByte); 23 | TSInputEdit.start_point(inputEdit, startPoint.into(allocator)); 24 | TSInputEdit.old_end_point(inputEdit, oldEndPoint.into(allocator)); 25 | TSInputEdit.new_end_point(inputEdit, newEndPoint.into(allocator)); 26 | return inputEdit; 27 | } 28 | 29 | @Override 30 | public String toString() { 31 | return String.format( 32 | "InputEdit[startByte=%s, oldEndByte=%s, newEndByte=%s, " 33 | + "startPoint=%s, oldEndPoint=%s, newEndPoint=%s]", 34 | Integer.toUnsignedString(startByte), 35 | Integer.toUnsignedString(oldEndByte), 36 | Integer.toUnsignedString(newEndByte), 37 | startPoint, 38 | oldEndPoint, 39 | newEndPoint); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/io/github/treesitter/jtreesitter/InputEncoding.java: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.jtreesitter; 2 | 3 | import java.nio.ByteOrder; 4 | import java.nio.charset.Charset; 5 | import java.nio.charset.StandardCharsets; 6 | import org.jspecify.annotations.NonNull; 7 | 8 | /** The encoding of source code. */ 9 | public enum InputEncoding { 10 | /** UTF-8 encoding. */ 11 | UTF_8(StandardCharsets.UTF_8), 12 | /** 13 | * UTF-16 little endian encoding. 14 | * 15 | * @since 0.25.0 16 | */ 17 | UTF_16LE(StandardCharsets.UTF_16LE), 18 | /** 19 | * UTF-16 big endian encoding. 20 | * 21 | * @since 0.25.0 22 | */ 23 | UTF_16BE(StandardCharsets.UTF_16BE); 24 | 25 | private final @NonNull Charset charset; 26 | 27 | InputEncoding(@NonNull Charset charset) { 28 | this.charset = charset; 29 | } 30 | 31 | Charset charset() { 32 | return charset; 33 | } 34 | 35 | private static final boolean IS_BIG_ENDIAN = ByteOrder.nativeOrder().equals(ByteOrder.BIG_ENDIAN); 36 | 37 | /** 38 | * Convert a standard {@linkplain Charset} to an {@linkplain InputEncoding}. 39 | * 40 | * @param charset one of {@link StandardCharsets#UTF_8}, {@link StandardCharsets#UTF_16BE}, 41 | * {@link StandardCharsets#UTF_16LE}, or {@link StandardCharsets#UTF_16} (native byte order). 42 | * @throws IllegalArgumentException If the character set is invalid. 43 | */ 44 | @SuppressWarnings("SameParameterValue") 45 | public static @NonNull InputEncoding valueOf(@NonNull Charset charset) throws IllegalArgumentException { 46 | if (charset.equals(StandardCharsets.UTF_8)) return InputEncoding.UTF_8; 47 | if (charset.equals(StandardCharsets.UTF_16BE)) return InputEncoding.UTF_16BE; 48 | if (charset.equals(StandardCharsets.UTF_16LE)) return InputEncoding.UTF_16LE; 49 | if (charset.equals(StandardCharsets.UTF_16)) { 50 | return IS_BIG_ENDIAN ? InputEncoding.UTF_16BE : InputEncoding.UTF_16LE; 51 | } 52 | throw new IllegalArgumentException("Invalid character set: %s".formatted(charset)); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/io/github/treesitter/jtreesitter/Language.java: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.jtreesitter; 2 | 3 | import static io.github.treesitter.jtreesitter.internal.TreeSitter.*; 4 | 5 | import io.github.treesitter.jtreesitter.internal.TSLanguageMetadata; 6 | import java.lang.foreign.*; 7 | import org.jspecify.annotations.NullMarked; 8 | import org.jspecify.annotations.Nullable; 9 | 10 | /** A class that defines how to parse a particular language. */ 11 | @NullMarked 12 | public final class Language implements Cloneable { 13 | /** 14 | * The latest ABI version that is supported by the current version of the library. 15 | * 16 | * @apiNote The Tree-sitter library is generally backwards-compatible with 17 | * languages generated using older CLI versions, but is not forwards-compatible. 18 | */ 19 | public static final @Unsigned int LANGUAGE_VERSION = TREE_SITTER_LANGUAGE_VERSION(); 20 | 21 | /** The earliest ABI version that is supported by the current version of the library. */ 22 | public static final @Unsigned int MIN_COMPATIBLE_LANGUAGE_VERSION = TREE_SITTER_MIN_COMPATIBLE_LANGUAGE_VERSION(); 23 | 24 | private static final ValueLayout VOID_PTR = 25 | ValueLayout.ADDRESS.withTargetLayout(MemoryLayout.sequenceLayout(Long.MAX_VALUE, ValueLayout.JAVA_BYTE)); 26 | 27 | private static final FunctionDescriptor FUNC_DESC = FunctionDescriptor.of(VOID_PTR); 28 | 29 | private static final Linker LINKER = Linker.nativeLinker(); 30 | 31 | private final MemorySegment self; 32 | 33 | private final @Unsigned int version; 34 | 35 | /** 36 | * Creates a new instance from the given language pointer. 37 | * 38 | * @implNote It is up to the caller to ensure that the pointer is valid. 39 | * 40 | * @throws IllegalArgumentException If the language version is incompatible. 41 | */ 42 | public Language(MemorySegment self) throws IllegalArgumentException { 43 | this.self = self.asReadOnly(); 44 | version = ts_language_abi_version(this.self); 45 | if (version < MIN_COMPATIBLE_LANGUAGE_VERSION || version > LANGUAGE_VERSION) { 46 | throw new IllegalArgumentException(String.format( 47 | "Incompatible language version %d. Must be between %d and %d.", 48 | version, MIN_COMPATIBLE_LANGUAGE_VERSION, LANGUAGE_VERSION)); 49 | } 50 | } 51 | 52 | private static UnsatisfiedLinkError unresolved(String name) { 53 | return new UnsatisfiedLinkError("Unresolved symbol: %s".formatted(name)); 54 | } 55 | 56 | /** 57 | * Load a language by looking for its function in the given symbols. 58 | * 59 | *

Example

60 | * 61 | *

{@snippet lang="java" : 62 | * String library = System.mapLibraryName("tree-sitter-java"); 63 | * SymbolLookup symbols = SymbolLookup.libraryLookup(library, Arena.global()); 64 | * Language language = Language.load(symbols, "tree_sitter_java"); 65 | * } 66 | *

67 | * The {@linkplain Arena} used to load the language 68 | * must not be closed while the language is being used. 69 | * 70 | * @throws RuntimeException If the language could not be loaded. 71 | * @since 0.23.1 72 | */ 73 | // TODO: deprecate when the bindings are generated by the CLI 74 | public static Language load(SymbolLookup symbols, String language) throws RuntimeException { 75 | var address = symbols.find(language).orElseThrow(() -> unresolved(language)); 76 | try { 77 | var function = LINKER.downcallHandle(address, FUNC_DESC); 78 | return new Language((MemorySegment) function.invokeExact()); 79 | } catch (Throwable e) { 80 | throw new RuntimeException("Failed to load %s".formatted(language), e); 81 | } 82 | } 83 | 84 | MemorySegment segment() { 85 | return self; 86 | } 87 | 88 | /** 89 | * Get the ABI version number for this language. 90 | * 91 | *

This version number is used to ensure that languages 92 | * were generated by a compatible version of Tree-sitter. 93 | * 94 | * @since 0.25.0 95 | */ 96 | public @Unsigned int getAbiVersion() { 97 | return version; 98 | } 99 | 100 | /** 101 | * Get the ABI version number for this language. 102 | * 103 | * @deprecated Use {@link #getAbiVersion} instead. 104 | */ 105 | @Deprecated(since = "0.25.0", forRemoval = true) 106 | public @Unsigned int getVersion() { 107 | return version; 108 | } 109 | 110 | /** Get the name of this language, if available. */ 111 | public @Nullable String getName() { 112 | var name = ts_language_name(self); 113 | return name.equals(MemorySegment.NULL) ? null : name.getString(0); 114 | } 115 | 116 | /** 117 | * Get the metadata for this language, if available. 118 | * 119 | * @apiNote This information is generated by the Tree-sitter 120 | * CLI and relies on the language author providing the correct 121 | * metadata in the language's {@code tree-sitter.json} file. 122 | * 123 | * @since 0.25.0 124 | */ 125 | public @Nullable LanguageMetadata getMetadata() { 126 | var metadata = ts_language_metadata(self); 127 | if (metadata.equals(MemorySegment.NULL)) return null; 128 | 129 | short major = TSLanguageMetadata.major_version(metadata); 130 | short minor = TSLanguageMetadata.minor_version(metadata); 131 | short patch = TSLanguageMetadata.patch_version(metadata); 132 | var version = new LanguageMetadata.Version(major, minor, patch); 133 | return new LanguageMetadata(version); 134 | } 135 | 136 | /** Get the number of distinct node types in this language. */ 137 | public @Unsigned int getSymbolCount() { 138 | return ts_language_symbol_count(self); 139 | } 140 | 141 | /** Get the number of valid states in this language */ 142 | public @Unsigned int getStateCount() { 143 | return ts_language_state_count(self); 144 | } 145 | 146 | /** Get the number of distinct field names in this language */ 147 | public @Unsigned int getFieldCount() { 148 | return ts_language_field_count(self); 149 | } 150 | 151 | /** 152 | * Get all supertype symbols for the language. 153 | * 154 | * @since 0.25.0 155 | */ 156 | public @Unsigned short[] getSupertypes() { 157 | try (var alloc = Arena.ofConfined()) { 158 | var length = alloc.allocate(C_INT.byteSize(), C_INT.byteAlignment()); 159 | var supertypes = ts_language_supertypes(self, length); 160 | var isEmpty = length.get(C_INT, 0) == 0; 161 | return isEmpty ? new short[0] : supertypes.toArray(C_SHORT); 162 | } 163 | } 164 | 165 | /** 166 | * Get all symbols for a given supertype symbol. 167 | * 168 | * @since 0.25.0 169 | * @see #getSupertypes() 170 | */ 171 | public @Unsigned short[] getSubtypes(@Unsigned short supertype) { 172 | try (var alloc = Arena.ofConfined()) { 173 | var length = alloc.allocate(C_INT.byteSize(), C_INT.byteAlignment()); 174 | var subtypes = ts_language_subtypes(self, supertype, length); 175 | var isEmpty = length.get(C_INT, 0) == 0; 176 | return isEmpty ? new short[0] : subtypes.toArray(C_SHORT); 177 | } 178 | } 179 | 180 | /** Get the node type for the given numerical ID. */ 181 | public @Nullable String getSymbolName(@Unsigned short symbol) { 182 | var name = ts_language_symbol_name(self, symbol); 183 | return name.equals(MemorySegment.NULL) ? null : name.getString(0); 184 | } 185 | 186 | /** Get the numerical ID for the given node type, or {@code 0} if not found. */ 187 | public @Unsigned short getSymbolForName(String name, boolean isNamed) { 188 | try (var arena = Arena.ofConfined()) { 189 | var segment = arena.allocateFrom(name); 190 | return ts_language_symbol_for_name(self, segment, name.length(), isNamed); 191 | } 192 | } 193 | 194 | /** 195 | * Check if the node for the given numerical ID is named. 196 | * 197 | * @see Node#isNamed 198 | */ 199 | public boolean isNamed(@Unsigned short symbol) { 200 | return ts_language_symbol_type(self, symbol) == TSSymbolTypeRegular(); 201 | } 202 | 203 | /** Check if the node for the given numerical ID is visible. */ 204 | public boolean isVisible(@Unsigned short symbol) { 205 | return ts_language_symbol_type(self, symbol) <= TSSymbolTypeAnonymous(); 206 | } 207 | 208 | /** 209 | * Check if the node for the given numerical ID is a supertype. 210 | * 211 | * @since 0.24.0 212 | */ 213 | public boolean isSupertype(@Unsigned short symbol) { 214 | return ts_language_symbol_type(self, symbol) == TSSymbolTypeSupertype(); 215 | } 216 | 217 | /** Get the field name for the given numerical id. */ 218 | public @Nullable String getFieldNameForId(@Unsigned short id) { 219 | var name = ts_language_field_name_for_id(self, id); 220 | return name.equals(MemorySegment.NULL) ? null : name.getString(0); 221 | } 222 | 223 | /** Get the numerical ID for the given field name. */ 224 | public @Unsigned short getFieldIdForName(String name) { 225 | try (var arena = Arena.ofConfined()) { 226 | var segment = arena.allocateFrom(name); 227 | return ts_language_field_id_for_name(self, segment, name.length()); 228 | } 229 | } 230 | 231 | /** 232 | * Get the next parse state. 233 | * 234 | *

{@snippet lang="java" : 235 | * short state = language.nextState(node.getParseState(), node.getGrammarSymbol()); 236 | * } 237 | * 238 | *

Combine this with {@link #lookaheadIterator lookaheadIterator(state)} 239 | * to generate completion suggestions or valid symbols in {@index ERROR} nodes. 240 | */ 241 | public @Unsigned short nextState(@Unsigned short state, @Unsigned short symbol) { 242 | return ts_language_next_state(self, state, symbol); 243 | } 244 | 245 | /** 246 | * Create a new lookahead iterator for the given parse state. 247 | * 248 | * @throws IllegalArgumentException If the state is invalid for this language. 249 | */ 250 | public LookaheadIterator lookaheadIterator(@Unsigned short state) throws IllegalArgumentException { 251 | return new LookaheadIterator(self, state); 252 | } 253 | 254 | /** 255 | * Create a new query from a string containing one or more S-expression patterns. 256 | * 257 | * @throws QueryError If an error occurred while creating the query. 258 | * @deprecated Use the {@link Query#Query(Language, String) Query} constructor instead. 259 | */ 260 | @Deprecated(since = "0.25.0") 261 | public Query query(String source) throws QueryError { 262 | return new Query(this, source); 263 | } 264 | 265 | /** 266 | * Get another reference to the language. 267 | * 268 | * @since 0.24.0 269 | */ 270 | @Override 271 | @SuppressWarnings("MethodDoesntCallSuperMethod") 272 | public Language clone() { 273 | return new Language(ts_language_copy(self)); 274 | } 275 | 276 | @Override 277 | public boolean equals(Object o) { 278 | if (this == o) return true; 279 | if (!(o instanceof Language other)) return false; 280 | return self.equals(other.self); 281 | } 282 | 283 | @Override 284 | public int hashCode() { 285 | return Long.hashCode(self.address()); 286 | } 287 | 288 | @Override 289 | public String toString() { 290 | return "Language{id=0x%x, version=%d}".formatted(self.address(), version); 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /src/main/java/io/github/treesitter/jtreesitter/LanguageMetadata.java: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.jtreesitter; 2 | 3 | import org.jspecify.annotations.NullMarked; 4 | 5 | /** 6 | * The metadata associated with a {@linkplain Language}. 7 | * 8 | * @since 0.25.0 9 | */ 10 | @NullMarked 11 | public record LanguageMetadata(Version version) { 12 | /** 13 | * The Semantic Version of the {@linkplain Language}. 14 | * 15 | *

This version information may be used to signal if a given parser 16 | * is incompatible with existing queries when upgrading between versions. 17 | * 18 | * @since 0.25.0 19 | */ 20 | public record Version(@Unsigned short major, @Unsigned short minor, @Unsigned short patch) { 21 | @Override 22 | public String toString() { 23 | return "%d.%d.%d".formatted(major, minor, patch); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/io/github/treesitter/jtreesitter/Logger.java: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.jtreesitter; 2 | 3 | import java.util.function.BiConsumer; 4 | import org.jspecify.annotations.NonNull; 5 | 6 | /** A function that logs parsing results. */ 7 | @FunctionalInterface 8 | public interface Logger extends BiConsumer { 9 | /** 10 | * {@inheritDoc} 11 | * 12 | * @param type the log type 13 | * @param message the log message 14 | */ 15 | @Override 16 | void accept(@NonNull Type type, @NonNull String message); 17 | 18 | /** The type of a log message. */ 19 | enum Type { 20 | /** Lexer message. */ 21 | LEX, 22 | /** Parser message. */ 23 | PARSE 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/io/github/treesitter/jtreesitter/LookaheadIterator.java: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.jtreesitter; 2 | 3 | import static io.github.treesitter.jtreesitter.internal.TreeSitter.*; 4 | 5 | import io.github.treesitter.jtreesitter.internal.TreeSitter; 6 | import java.lang.foreign.Arena; 7 | import java.lang.foreign.MemorySegment; 8 | import java.util.*; 9 | import java.util.function.Consumer; 10 | import java.util.stream.Stream; 11 | import java.util.stream.StreamSupport; 12 | import org.jspecify.annotations.NullMarked; 13 | 14 | /** 15 | * A class that is used to look up valid symbols in a specific parse state. 16 | * 17 | *

Lookahead iterators can be useful to generate suggestions and improve syntax error diagnostics.
18 | * To get symbols valid in an {@index ERROR} node, use the lookahead iterator on its first leaf node state.
19 | * For {@index MISSING} nodes, a lookahead iterator created on the previous non-extra leaf node may be appropriate. 20 | */ 21 | @NullMarked 22 | public final class LookaheadIterator implements AutoCloseable, Iterator { 23 | private final Arena arena; 24 | private final MemorySegment self; 25 | private final short state; 26 | private boolean iterFirst = true; 27 | private boolean hasNext = false; 28 | 29 | LookaheadIterator(MemorySegment language, @Unsigned short state) throws IllegalArgumentException { 30 | var iterator = ts_lookahead_iterator_new(language, state); 31 | if (iterator == null) { 32 | throw new IllegalArgumentException( 33 | "State %d is not valid for %s".formatted(Short.toUnsignedInt(state), this)); 34 | } 35 | this.state = state; 36 | arena = Arena.ofShared(); 37 | self = iterator.reinterpret(arena, TreeSitter::ts_lookahead_iterator_delete); 38 | } 39 | 40 | /** Get the current language of the lookahead iterator. */ 41 | public Language getLanguage() { 42 | return new Language(ts_lookahead_iterator_language(self)); 43 | } 44 | 45 | /** 46 | * Get the current symbol ID. 47 | * 48 | * @apiNote The ID of the {@index ERROR} symbol is equal to {@code -1}. 49 | */ 50 | public @Unsigned short getCurrentSymbol() { 51 | return ts_lookahead_iterator_current_symbol(self); 52 | } 53 | 54 | /** 55 | * The current symbol name. 56 | * 57 | * @apiNote Newly created lookahead iterators will contain the {@index ERROR} symbol. 58 | */ 59 | public String getCurrentSymbolName() { 60 | return ts_lookahead_iterator_current_symbol_name(self).getString(0); 61 | } 62 | 63 | /** 64 | * Reset the lookahead iterator to the given state. 65 | * 66 | * @return {@code true} if the iterator was reset 67 | * successfully or {@code false} if it failed. 68 | */ 69 | public boolean reset(@Unsigned short state) { 70 | return ts_lookahead_iterator_reset_state(self, state); 71 | } 72 | 73 | /** 74 | * Reset the lookahead iterator to the given state and another language. 75 | * 76 | * @return {@code true} if the iterator was reset 77 | * successfully or {@code false} if it failed. 78 | */ 79 | public boolean reset(@Unsigned short state, Language language) { 80 | return ts_lookahead_iterator_reset(self, language.segment(), state); 81 | } 82 | 83 | /** Check if the lookahead iterator has more symbols. */ 84 | @Override 85 | public boolean hasNext() { 86 | if (iterFirst) { 87 | iterFirst = false; 88 | hasNext = ts_lookahead_iterator_next(self); 89 | ts_lookahead_iterator_reset_state(self, state); 90 | } 91 | return hasNext; 92 | } 93 | 94 | /** 95 | * Advance the lookahead iterator to the next symbol. 96 | * 97 | * @throws NoSuchElementException If there are no more symbols. 98 | */ 99 | @Override 100 | public Symbol next() throws NoSuchElementException { 101 | if (!hasNext()) throw new NoSuchElementException(); 102 | hasNext = ts_lookahead_iterator_next(self); 103 | return new Symbol(getCurrentSymbol(), getCurrentSymbolName()); 104 | } 105 | 106 | /** 107 | * Iterate over the symbol IDs. 108 | * 109 | * @implNote Calling this method will reset the iterator to its original state. 110 | */ 111 | public @Unsigned Stream symbols() { 112 | ts_lookahead_iterator_reset_state(self, state); 113 | return StreamSupport.stream(new IdIterator(self), false); 114 | } 115 | 116 | /** 117 | * Iterate over the symbol names. 118 | * 119 | * @implNote Calling this method will reset the iterator to its original state. 120 | */ 121 | public Stream names() { 122 | ts_lookahead_iterator_reset_state(self, state); 123 | return StreamSupport.stream(new NameIterator(self), false); 124 | } 125 | 126 | @Override 127 | public void close() throws RuntimeException { 128 | arena.close(); 129 | } 130 | 131 | /** @hidden */ 132 | @Override 133 | public void remove() { 134 | Iterator.super.remove(); 135 | } 136 | 137 | /** A class that pairs a symbol ID with its name. */ 138 | public record Symbol(@Unsigned short id, String name) {} 139 | 140 | private static final class IdIterator extends Spliterators.AbstractSpliterator { 141 | private final MemorySegment iterator; 142 | 143 | private IdIterator(MemorySegment iterator) { 144 | super(Long.MAX_VALUE, Spliterator.IMMUTABLE | Spliterator.SORTED | Spliterator.ORDERED); 145 | this.iterator = iterator; 146 | } 147 | 148 | @Override 149 | public Comparator getComparator() { 150 | return Short::compareUnsigned; 151 | } 152 | 153 | @Override 154 | public boolean tryAdvance(Consumer action) { 155 | var result = ts_lookahead_iterator_next(iterator); 156 | if (result) { 157 | var symbol = ts_lookahead_iterator_current_symbol(iterator); 158 | action.accept(symbol); 159 | } 160 | return result; 161 | } 162 | } 163 | 164 | private static final class NameIterator extends Spliterators.AbstractSpliterator { 165 | private final MemorySegment iterator; 166 | 167 | private NameIterator(MemorySegment iterator) { 168 | super(Long.MAX_VALUE, Spliterator.IMMUTABLE | Spliterator.NONNULL); 169 | this.iterator = iterator; 170 | } 171 | 172 | @Override 173 | public boolean tryAdvance(Consumer action) { 174 | var result = ts_lookahead_iterator_next(iterator); 175 | if (result) { 176 | var name = ts_lookahead_iterator_current_symbol_name(iterator); 177 | action.accept(name.getString(0)); 178 | } 179 | return result; 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/main/java/io/github/treesitter/jtreesitter/MatchesIterator.java: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.jtreesitter; 2 | 3 | import static io.github.treesitter.jtreesitter.internal.TreeSitter.ts_query_cursor_next_match; 4 | 5 | import io.github.treesitter.jtreesitter.internal.TSQueryMatch; 6 | import java.lang.foreign.MemorySegment; 7 | import java.lang.foreign.SegmentAllocator; 8 | import java.util.Spliterator; 9 | import java.util.Spliterators; 10 | import java.util.function.BiPredicate; 11 | import java.util.function.Consumer; 12 | import org.jspecify.annotations.NullMarked; 13 | import org.jspecify.annotations.Nullable; 14 | 15 | @NullMarked 16 | class MatchesIterator extends Spliterators.AbstractSpliterator { 17 | private final @Nullable BiPredicate predicate; 18 | private final Tree tree; 19 | private final SegmentAllocator allocator; 20 | private final Query query; 21 | private final MemorySegment cursor; 22 | 23 | public MatchesIterator( 24 | Query query, 25 | MemorySegment cursor, 26 | Tree tree, 27 | SegmentAllocator allocator, 28 | @Nullable BiPredicate predicate) { 29 | super(Long.MAX_VALUE, Spliterator.IMMUTABLE | Spliterator.NONNULL); 30 | this.predicate = predicate; 31 | this.tree = tree; 32 | this.allocator = allocator; 33 | this.query = query; 34 | this.cursor = cursor; 35 | } 36 | 37 | @Override 38 | public boolean tryAdvance(Consumer action) { 39 | var hasNoText = tree.getText() == null; 40 | MemorySegment match = allocator.allocate(TSQueryMatch.layout()); 41 | var captureNames = query.getCaptureNames(); 42 | while (ts_query_cursor_next_match(cursor, match)) { 43 | var result = QueryMatch.from(match, captureNames, tree, allocator); 44 | if (hasNoText || query.matches(predicate, result)) { 45 | action.accept(result); 46 | return true; 47 | } 48 | } 49 | return false; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/io/github/treesitter/jtreesitter/NativeLibraryLookup.java: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.jtreesitter; 2 | 3 | import java.lang.foreign.Arena; 4 | import java.lang.foreign.SymbolLookup; 5 | 6 | /** 7 | * An interface implemented by clients that wish to customize the {@link SymbolLookup} 8 | * used for the tree-sitter native library. Implementations must be registered 9 | * by listing their fully qualified class name in a resource file named 10 | * {@code META-INF/services/io.github.treesitter.jtreesitter.NativeLibraryLookup}. 11 | * 12 | * @since 0.25.0 13 | * @see java.util.ServiceLoader 14 | */ 15 | @FunctionalInterface 16 | public interface NativeLibraryLookup { 17 | /** 18 | * Get the {@link SymbolLookup} to be used for the tree-sitter native library. 19 | * 20 | * @param arena The arena that will manage the native memory. 21 | * @since 0.25.0 22 | */ 23 | SymbolLookup get(Arena arena); 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/io/github/treesitter/jtreesitter/Node.java: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.jtreesitter; 2 | 3 | import static io.github.treesitter.jtreesitter.internal.TreeSitter.*; 4 | 5 | import io.github.treesitter.jtreesitter.internal.TSNode; 6 | import java.lang.foreign.Arena; 7 | import java.lang.foreign.MemorySegment; 8 | import java.util.*; 9 | import org.jspecify.annotations.NullMarked; 10 | import org.jspecify.annotations.Nullable; 11 | 12 | /** 13 | * A single node within a {@linkplain Tree syntax tree}. 14 | * 15 | * @implNote Node lifetimes are tied to the {@link Tree}, 16 | * {@link TreeCursor}, or {@link Query} that they belong to. 17 | */ 18 | @NullMarked 19 | public final class Node { 20 | private final MemorySegment self; 21 | private final Tree tree; 22 | private @Nullable List children; 23 | private final Arena arena = Arena.ofAuto(); 24 | private boolean wasEdited = false; 25 | 26 | Node(MemorySegment self, Tree tree) { 27 | this.self = self; 28 | this.tree = tree; 29 | } 30 | 31 | private Optional optional(MemorySegment node) { 32 | return ts_node_is_null(node) ? Optional.empty() : Optional.of(new Node(node, tree)); 33 | } 34 | 35 | MemorySegment copy(Arena arena) { 36 | return self.reinterpret(arena, null); 37 | } 38 | 39 | /** Get the tree that contains this node. */ 40 | public Tree getTree() { 41 | return tree; 42 | } 43 | 44 | /** 45 | * Get the numerical ID of the node. 46 | * 47 | * @apiNote Within any given syntax tree, no two nodes have the same ID. However, 48 | * if a new tree is created based on an older tree, and a node from the old tree 49 | * is reused in the process, then that node will have the same ID in both trees. 50 | */ 51 | public @Unsigned long getId() { 52 | return TSNode.id(self).address(); 53 | } 54 | 55 | /** Get the numerical ID of the node's type. */ 56 | public @Unsigned short getSymbol() { 57 | return ts_node_symbol(self); 58 | } 59 | 60 | /** Get the numerical ID of the node's type, as it appears in the grammar ignoring aliases. */ 61 | public @Unsigned short getGrammarSymbol() { 62 | return ts_node_grammar_symbol(self); 63 | } 64 | 65 | /** Get the type of the node. */ 66 | public String getType() { 67 | return ts_node_type(self).getString(0); 68 | } 69 | 70 | /** Get the type of the node, as it appears in the grammar ignoring aliases. */ 71 | public String getGrammarType() { 72 | return ts_node_grammar_type(self).getString(0); 73 | } 74 | 75 | /** 76 | * Check if the node is named. 77 | * 78 | *

Named nodes correspond to named rules in the grammar, 79 | * whereas anonymous nodes correspond to string literals. 80 | */ 81 | public boolean isNamed() { 82 | return ts_node_is_named(self); 83 | } 84 | 85 | /** 86 | * Check if the node is extra. 87 | * 88 | *

Extra nodes represent things which are not required 89 | * by the grammar but can appear anywhere (e.g. whitespace). 90 | */ 91 | public boolean isExtra() { 92 | return ts_node_is_extra(self); 93 | } 94 | 95 | /** Check if the node is an {@index ERROR} node. */ 96 | public boolean isError() { 97 | return ts_node_is_error(self); 98 | } 99 | 100 | /** 101 | * Check if the node is {@index MISSING}. 102 | * 103 | *

MISSING nodes are inserted by the parser in order 104 | * to recover from certain kinds of syntax errors. 105 | */ 106 | public boolean isMissing() { 107 | return ts_node_is_missing(self); 108 | } 109 | 110 | /** Check if the node has been edited. */ 111 | public boolean hasChanges() { 112 | return ts_node_has_changes(self); 113 | } 114 | 115 | /** 116 | * Check if the node is an {@index ERROR}, 117 | * or contains any {@index ERROR} nodes. 118 | */ 119 | public boolean hasError() { 120 | return ts_node_has_error(self); 121 | } 122 | 123 | /** Get the parse state of this node. */ 124 | public @Unsigned short getParseState() { 125 | return ts_node_parse_state(self); 126 | } 127 | 128 | /** Get the parse state after this node. */ 129 | public @Unsigned short getNextParseState() { 130 | return ts_node_next_parse_state(self); 131 | } 132 | 133 | /** Get the start byte of the node. */ 134 | public @Unsigned int getStartByte() { 135 | return ts_node_start_byte(self); 136 | } 137 | 138 | /** Get the end byte of the node. */ 139 | public @Unsigned int getEndByte() { 140 | return ts_node_end_byte(self); 141 | } 142 | 143 | /** Get the range of the node. */ 144 | public Range getRange() { 145 | return new Range(getStartPoint(), getEndPoint(), getStartByte(), getEndByte()); 146 | } 147 | 148 | /** Get the start point of the node. */ 149 | public Point getStartPoint() { 150 | return Point.from(ts_node_start_point(arena, self)); 151 | } 152 | 153 | /** Get the end point of the node. */ 154 | public Point getEndPoint() { 155 | return Point.from(ts_node_end_point(arena, self)); 156 | } 157 | 158 | /** Get the number of this node's children. */ 159 | public @Unsigned int getChildCount() { 160 | return ts_node_child_count(self); 161 | } 162 | 163 | /** Get the number of this node's named children. */ 164 | public @Unsigned int getNamedChildCount() { 165 | return ts_node_named_child_count(self); 166 | } 167 | 168 | /** Get the number of this node's descendants, including the node itself. */ 169 | public @Unsigned int getDescendantCount() { 170 | return ts_node_descendant_count(self); 171 | } 172 | 173 | /** The node's immediate parent, if any. */ 174 | public Optional getParent() { 175 | return optional(ts_node_parent(arena, self)); 176 | } 177 | 178 | /** The node's next sibling, if any. */ 179 | public Optional getNextSibling() { 180 | return optional(ts_node_next_sibling(arena, self)); 181 | } 182 | 183 | /** The node's previous sibling, if any. */ 184 | public Optional getPrevSibling() { 185 | return optional(ts_node_prev_sibling(arena, self)); 186 | } 187 | 188 | /** The node's next named sibling, if any. */ 189 | public Optional getNextNamedSibling() { 190 | return optional(ts_node_next_named_sibling(arena, self)); 191 | } 192 | 193 | /** The node's previous named sibling, if any. */ 194 | public Optional getPrevNamedSibling() { 195 | return optional(ts_node_prev_named_sibling(arena, self)); 196 | } 197 | 198 | /** 199 | * Get the node's child at the given index, if any. 200 | * 201 | * @apiNote This method is fairly fast, but its cost is technically 202 | * {@code log(i)}, so if you might be iterating over a long list of children, 203 | * you should use {@link #getChildren()} or {@link #walk()} instead. 204 | * 205 | * @throws IndexOutOfBoundsException If the index exceeds the 206 | * {@linkplain #getChildCount() child count}. 207 | */ 208 | public Optional getChild(@Unsigned int index) throws IndexOutOfBoundsException { 209 | if (index >= getChildCount()) { 210 | throw new IndexOutOfBoundsException( 211 | "Child index %s is out of bounds".formatted(Integer.toUnsignedString(index))); 212 | } 213 | return optional(ts_node_child(arena, self, index)); 214 | } 215 | 216 | /** 217 | * Get the node's named child at the given index, if any. 218 | * 219 | * @apiNote This method is fairly fast, but its cost is technically 220 | * {@code log(i)}, so if you might be iterating over a long list of children, 221 | * you should use {@link #getNamedChildren()} or {@link #walk()} instead. 222 | * 223 | * @throws IndexOutOfBoundsException If the index exceeds the 224 | * {@linkplain #getNamedChildCount() child count}. 225 | */ 226 | public Optional getNamedChild(@Unsigned int index) throws IndexOutOfBoundsException { 227 | if (index >= getNamedChildCount()) { 228 | throw new IndexOutOfBoundsException( 229 | "Child index %s is out of bounds".formatted(Integer.toUnsignedString(index))); 230 | } 231 | return optional(ts_node_named_child(arena, self, index)); 232 | } 233 | 234 | /** 235 | * Get the node's first child that contains or starts after the given byte offset. 236 | * 237 | * @since 0.25.0 238 | */ 239 | public Optional getFirstChildForByte(@Unsigned int byte_offset) { 240 | return optional(ts_node_first_child_for_byte(arena, self, byte_offset)); 241 | } 242 | 243 | /** 244 | * Get the node's first named child that contains or starts after the given byte offset. 245 | * 246 | * @since 0.25.0 247 | */ 248 | public Optional getFirstNamedChildForByte(@Unsigned int byte_offset) { 249 | return optional(ts_node_first_named_child_for_byte(arena, self, byte_offset)); 250 | } 251 | 252 | /** 253 | * Get the node's first child with the given field ID, if any. 254 | * 255 | * @see Language#getFieldIdForName 256 | */ 257 | public Optional getChildByFieldId(@Unsigned short id) { 258 | return optional(ts_node_child_by_field_id(arena, self, id)); 259 | } 260 | 261 | /** Get the node's first child with the given field name, if any. */ 262 | public Optional getChildByFieldName(String name) { 263 | var segment = arena.allocateFrom(name); 264 | return optional(ts_node_child_by_field_name(arena, self, segment, name.length())); 265 | } 266 | 267 | /** 268 | * Get this node's children. 269 | * 270 | * @apiNote If you're walking the tree recursively, you may want to use {@link #walk()} instead. 271 | */ 272 | public List getChildren() { 273 | if (this.children == null) { 274 | var length = getChildCount(); 275 | if (length == 0) return Collections.emptyList(); 276 | var children = new ArrayList(length); 277 | var cursor = ts_tree_cursor_new(arena, self); 278 | ts_tree_cursor_goto_first_child(cursor); 279 | for (int i = 0; i < length; ++i) { 280 | var node = ts_tree_cursor_current_node(arena, cursor); 281 | children.add(new Node(node, tree)); 282 | ts_tree_cursor_goto_next_sibling(cursor); 283 | } 284 | ts_tree_cursor_delete(cursor); 285 | this.children = Collections.unmodifiableList(children); 286 | } 287 | return this.children; 288 | } 289 | 290 | /** Get this node's named children. */ 291 | public List getNamedChildren() { 292 | return getChildren().stream().filter(Node::isNamed).toList(); 293 | } 294 | 295 | /** 296 | * Get a list of the node's children with the given field ID. 297 | * 298 | * @see Language#getFieldIdForName 299 | */ 300 | public List getChildrenByFieldId(@Unsigned short id) { 301 | if (id == 0) return Collections.emptyList(); 302 | var length = getChildCount(); 303 | var children = new ArrayList(length); 304 | var cursor = ts_tree_cursor_new(arena, self); 305 | var ok = ts_tree_cursor_goto_first_child(cursor); 306 | while (ok) { 307 | if (ts_tree_cursor_current_field_id(cursor) == id) { 308 | var node = ts_tree_cursor_current_node(arena, cursor); 309 | children.add(new Node(node, tree)); 310 | } 311 | ok = ts_tree_cursor_goto_next_sibling(cursor); 312 | } 313 | ts_tree_cursor_delete(cursor); 314 | children.trimToSize(); 315 | return children; 316 | } 317 | 318 | /** Get a list of the node's child with the given field name. */ 319 | public List getChildrenByFieldName(String name) { 320 | return getChildrenByFieldId(tree.getLanguage().getFieldIdForName(name)); 321 | } 322 | 323 | /** 324 | * Get the field name of this node’s child at the given index, if available. 325 | * 326 | * @throws IndexOutOfBoundsException If the index exceeds the 327 | * {@linkplain #getNamedChildCount() child count}. 328 | */ 329 | public @Nullable String getFieldNameForChild(@Unsigned int index) throws IndexOutOfBoundsException { 330 | if (index >= getChildCount()) { 331 | throw new IndexOutOfBoundsException( 332 | "Child index %s is out of bounds".formatted(Integer.toUnsignedString(index))); 333 | } 334 | var segment = ts_node_field_name_for_child(self, index); 335 | return segment.equals(MemorySegment.NULL) ? null : segment.getString(0); 336 | } 337 | 338 | /** 339 | * Get the field name of this node's named child at the given index, if available. 340 | * 341 | * @throws IndexOutOfBoundsException If the index exceeds the 342 | * {@linkplain #getNamedChildCount() child count}. 343 | * @since 0.24.0 344 | */ 345 | public @Nullable String getFieldNameForNamedChild(@Unsigned int index) throws IndexOutOfBoundsException { 346 | if (index >= getChildCount()) { 347 | throw new IndexOutOfBoundsException( 348 | "Child index %s is out of bounds".formatted(Integer.toUnsignedString(index))); 349 | } 350 | var segment = ts_node_field_name_for_named_child(self, index); 351 | return segment.equals(MemorySegment.NULL) ? null : segment.getString(0); 352 | } 353 | 354 | /** 355 | * Get the smallest node within this node that spans the given byte range, if any. 356 | * 357 | * @throws IllegalArgumentException If {@code start > end}. 358 | */ 359 | public Optional getDescendant(@Unsigned int start, @Unsigned int end) throws IllegalArgumentException { 360 | if (Integer.compareUnsigned(start, end) > 0) { 361 | throw new IllegalArgumentException(String.format( 362 | "Start byte %s exceeds end byte %s", 363 | Integer.toUnsignedString(start), Integer.toUnsignedString(end))); 364 | } 365 | return optional(ts_node_descendant_for_byte_range(arena, self, start, end)); 366 | } 367 | 368 | /** 369 | * Get the smallest node within this node that spans the given point range, if any. 370 | * 371 | * @throws IllegalArgumentException If {@code start > end}. 372 | */ 373 | public Optional getDescendant(Point start, Point end) throws IllegalArgumentException { 374 | if (start.compareTo(end) > 0) { 375 | throw new IllegalArgumentException("Start point %s exceeds end point %s".formatted(start, end)); 376 | } 377 | MemorySegment startPoint = start.into(arena), endPoint = end.into(arena); 378 | return optional(ts_node_descendant_for_point_range(arena, self, startPoint, endPoint)); 379 | } 380 | 381 | /** 382 | * Get the smallest named node within this node that spans the given byte range, if any. 383 | * 384 | * @throws IllegalArgumentException If {@code start > end}. 385 | */ 386 | public Optional getNamedDescendant(@Unsigned int start, @Unsigned int end) throws IllegalArgumentException { 387 | if (Integer.compareUnsigned(start, end) > 0) { 388 | throw new IllegalArgumentException(String.format( 389 | "Start byte %s exceeds end byte %s", 390 | Integer.toUnsignedString(start), Integer.toUnsignedString(end))); 391 | } 392 | return optional(ts_node_named_descendant_for_byte_range(arena, self, start, end)); 393 | } 394 | 395 | /** 396 | * Get the smallest named node within this node that spans the given point range, if any. 397 | * 398 | * @throws IllegalArgumentException If {@code start > end}. 399 | */ 400 | public Optional getNamedDescendant(Point start, Point end) { 401 | if (start.compareTo(end) > 0) { 402 | throw new IllegalArgumentException("Start point %s exceeds end point %s".formatted(start, end)); 403 | } 404 | MemorySegment startPoint = start.into(arena), endPoint = end.into(arena); 405 | return optional(ts_node_named_descendant_for_point_range(arena, self, startPoint, endPoint)); 406 | } 407 | 408 | /** 409 | * Get the node that contains the given descendant, if any. 410 | * 411 | * @since 0.24.0 412 | */ 413 | public Optional getChildWithDescendant(Node descendant) { 414 | return optional(ts_node_child_with_descendant(arena, self, descendant.self)); 415 | } 416 | 417 | /** Get the source code of the node, if available. */ 418 | public @Nullable String getText() { 419 | return !wasEdited ? tree.getRegion(getStartByte(), getEndByte()) : null; 420 | } 421 | 422 | /** 423 | * Edit this node to keep it in-sync with source code that has been edited. 424 | * 425 | * @apiNote This method is only rarely needed. When you edit a syntax 426 | * tree via {@link Tree#edit}, all of the nodes that you retrieve from 427 | * the tree afterward will already reflect the edit. You only need 428 | * to use this when you have a specific {@linkplain Node} instance 429 | * that you want to keep and continue to use after an edit. 430 | */ 431 | public void edit(InputEdit edit) { 432 | ts_node_edit(self, edit.into(arena)); 433 | wasEdited = true; 434 | children = null; 435 | } 436 | 437 | /** Create a new {@linkplain TreeCursor tree cursor} starting from this node. */ 438 | public TreeCursor walk() { 439 | return new TreeCursor(this, tree); 440 | } 441 | 442 | /** Get the S-expression representing the node. */ 443 | public String toSexp() { 444 | var string = ts_node_string(self); 445 | var result = string.getString(0); 446 | free(string); 447 | return result; 448 | } 449 | 450 | /** Check if two nodes are identical. */ 451 | @Override 452 | public boolean equals(Object o) { 453 | if (this == o) return true; 454 | if (!(o instanceof Node other)) return false; 455 | return ts_node_eq(self, other.self); 456 | } 457 | 458 | @Override 459 | public int hashCode() { 460 | return Long.hashCode(getId()); 461 | } 462 | 463 | @Override 464 | public String toString() { 465 | return String.format( 466 | "Node{type=%s, startByte=%s, endByte=%s}", 467 | getType(), Integer.toUnsignedString(getStartByte()), Integer.toUnsignedString(getEndByte())); 468 | } 469 | } 470 | -------------------------------------------------------------------------------- /src/main/java/io/github/treesitter/jtreesitter/ParseCallback.java: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.jtreesitter; 2 | 3 | import java.util.function.BiFunction; 4 | import org.jspecify.annotations.NonNull; 5 | import org.jspecify.annotations.Nullable; 6 | 7 | /** A function that retrieves a chunk of text at a given byte offset and point. */ 8 | @FunctionalInterface 9 | public interface ParseCallback extends BiFunction { 10 | /** 11 | * {@inheritDoc} 12 | * 13 | * @param offset the current byte offset 14 | * @param point the current point 15 | * @return A chunk of text or {@code null} to indicate the end of the document. 16 | */ 17 | @Override 18 | @Nullable 19 | String apply(@Unsigned Integer offset, @NonNull Point point); 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/io/github/treesitter/jtreesitter/Parser.java: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.jtreesitter; 2 | 3 | import static io.github.treesitter.jtreesitter.internal.TreeSitter.*; 4 | 5 | import io.github.treesitter.jtreesitter.internal.*; 6 | import java.lang.foreign.Arena; 7 | import java.lang.foreign.MemoryLayout; 8 | import java.lang.foreign.MemorySegment; 9 | import java.util.Collections; 10 | import java.util.List; 11 | import java.util.Optional; 12 | import java.util.concurrent.atomic.AtomicLong; 13 | import java.util.function.Predicate; 14 | import org.jspecify.annotations.NullMarked; 15 | import org.jspecify.annotations.Nullable; 16 | 17 | /** A class that is used to produce a {@linkplain Tree syntax tree} from source code. */ 18 | @NullMarked 19 | public final class Parser implements AutoCloseable { 20 | final MemorySegment self; 21 | private final Arena arena; 22 | private @Nullable Language language; 23 | private List includedRanges = Collections.singletonList(Range.DEFAULT); 24 | 25 | /** 26 | * Creates a new instance with a {@code null} language. 27 | * 28 | * @apiNote Parsing cannot be performed while the language is {@code null}. 29 | */ 30 | public Parser() { 31 | arena = Arena.ofShared(); 32 | self = ts_parser_new().reinterpret(arena, TreeSitter::ts_parser_delete); 33 | } 34 | 35 | /** Creates a new instance with the given language. */ 36 | public Parser(Language language) { 37 | this(); 38 | ts_parser_set_language(self, language.segment()); 39 | this.language = language; 40 | } 41 | 42 | /** Get the language that the parser will use for parsing. */ 43 | public @Nullable Language getLanguage() { 44 | return language; 45 | } 46 | 47 | /** Set the language that the parser will use for parsing. */ 48 | public Parser setLanguage(Language language) { 49 | ts_parser_set_language(self, language.segment()); 50 | this.language = language; 51 | return this; 52 | } 53 | 54 | /** 55 | * Get the maximum duration in microseconds that 56 | * parsing should be allowed to take before halting. 57 | * 58 | * @deprecated Use {@link Options} instead. 59 | */ 60 | @Deprecated(since = "0.25.0") 61 | public @Unsigned long getTimeoutMicros() { 62 | return ts_parser_timeout_micros(self); 63 | } 64 | 65 | /** 66 | * Set the maximum duration in microseconds that 67 | * parsing should be allowed to take before halting. 68 | * 69 | * @deprecated Use {@link Options} instead. 70 | */ 71 | @Deprecated(since = "0.25.0") 72 | @SuppressWarnings("DeprecatedIsStillUsed") 73 | public Parser setTimeoutMicros(@Unsigned long timeoutMicros) { 74 | ts_parser_set_timeout_micros(self, timeoutMicros); 75 | return this; 76 | } 77 | 78 | /** 79 | * Set the logger that the parser will use during parsing. 80 | * 81 | *

Example

82 | *

83 | * {@snippet lang="java" : 84 | * import java.util.logging.Logger; 85 | * 86 | * Logger logger = Logger.getLogger("tree-sitter"); 87 | * Parser parser = new Parser().setLogger( 88 | * (type, message) -> logger.info("%s - %s".formatted(type.name(), message))); 89 | * } 90 | */ 91 | @SuppressWarnings("unused") 92 | public Parser setLogger(@Nullable Logger logger) { 93 | if (logger == null) { 94 | ts_parser_set_logger(self, TSLogger.allocate(arena)); 95 | } else { 96 | var segment = TSLogger.allocate(arena); 97 | TSLogger.payload(segment, MemorySegment.NULL); 98 | // NOTE: can't use _ because of palantir/palantir-java-format#934 99 | var log = TSLogger.log.allocate( 100 | (p, type, message) -> { 101 | var logType = Logger.Type.values()[type]; 102 | logger.accept(logType, message.getString(0)); 103 | }, 104 | arena); 105 | TSLogger.log(segment, log); 106 | ts_parser_set_logger(self, segment); 107 | } 108 | return this; 109 | } 110 | 111 | /** 112 | * Set the parser's current cancellation flag. 113 | * 114 | *

The parser will periodically read from this flag during parsing. 115 | * If it reads a non-zero value, it will halt early. 116 | * 117 | * @deprecated Use {@link Options} instead. 118 | */ 119 | @Deprecated(since = "0.25.0") 120 | @SuppressWarnings("DeprecatedIsStillUsed") 121 | public synchronized Parser setCancellationFlag(CancellationFlag cancellationFlag) { 122 | ts_parser_set_cancellation_flag(self, cancellationFlag.segment); 123 | return this; 124 | } 125 | 126 | /** 127 | * Get the ranges of text that the parser should include when parsing. 128 | * 129 | * @apiNote By default, the parser will always include entire documents. 130 | */ 131 | public List getIncludedRanges() { 132 | return includedRanges; 133 | } 134 | 135 | /** 136 | * Set the ranges of text that the parser should include when parsing. 137 | * 138 | *

This allows you to parse only a portion of a document 139 | * but still return a syntax tree whose ranges match up with the 140 | * document as a whole. You can also pass multiple disjoint ranges. 141 | * 142 | * @throws IllegalArgumentException If the ranges overlap or are not in ascending order. 143 | */ 144 | public Parser setIncludedRanges(List includedRanges) { 145 | var size = includedRanges.size(); 146 | if (size > 0) { 147 | try (var arena = Arena.ofConfined()) { 148 | var layout = MemoryLayout.sequenceLayout(size, TSRange.layout()); 149 | var ranges = arena.allocate(layout); 150 | 151 | var startRow = layout.varHandle( 152 | MemoryLayout.PathElement.sequenceElement(), 153 | MemoryLayout.PathElement.groupElement("start_point"), 154 | MemoryLayout.PathElement.groupElement("row")); 155 | var startColumn = layout.varHandle( 156 | MemoryLayout.PathElement.sequenceElement(), 157 | MemoryLayout.PathElement.groupElement("start_point"), 158 | MemoryLayout.PathElement.groupElement("column")); 159 | var endRow = layout.varHandle( 160 | MemoryLayout.PathElement.sequenceElement(), 161 | MemoryLayout.PathElement.groupElement("end_point"), 162 | MemoryLayout.PathElement.groupElement("row")); 163 | var endColumn = layout.varHandle( 164 | MemoryLayout.PathElement.sequenceElement(), 165 | MemoryLayout.PathElement.groupElement("end_point"), 166 | MemoryLayout.PathElement.groupElement("column")); 167 | var startByte = layout.varHandle( 168 | MemoryLayout.PathElement.sequenceElement(), 169 | MemoryLayout.PathElement.groupElement("start_byte")); 170 | var endByte = layout.varHandle( 171 | MemoryLayout.PathElement.sequenceElement(), /**/ 172 | MemoryLayout.PathElement.groupElement("end_byte")); 173 | 174 | for (int i = 0; i < size; ++i) { 175 | var range = includedRanges.get(i).into(arena); 176 | var startPoint = TSRange.start_point(range); 177 | var endPoint = TSRange.end_point(range); 178 | startByte.set(ranges, 0L, (long) i, TSRange.start_byte(range)); 179 | endByte.set(ranges, 0L, (long) i, TSRange.end_byte(range)); 180 | startRow.set(ranges, 0L, (long) i, TSPoint.row(startPoint)); 181 | startColumn.set(ranges, 0L, (long) i, TSPoint.column(startPoint)); 182 | endRow.set(ranges, 0L, (long) i, TSPoint.row(endPoint)); 183 | endColumn.set(ranges, 0L, (long) i, TSPoint.column(endPoint)); 184 | } 185 | 186 | if (!ts_parser_set_included_ranges(self, ranges, size)) { 187 | throw new IllegalArgumentException( 188 | "Included ranges must be in ascending order and must not overlap"); 189 | } 190 | } 191 | this.includedRanges = List.copyOf(includedRanges); 192 | } else { 193 | ts_parser_set_included_ranges(self, MemorySegment.NULL, 0); 194 | this.includedRanges = Collections.singletonList(Range.DEFAULT); 195 | } 196 | return this; 197 | } 198 | 199 | /** 200 | * Parse source code from a string and create a syntax tree. 201 | * 202 | * @return An optional {@linkplain Tree} which is empty if parsing was halted. 203 | * @throws IllegalStateException If the parser does not have a language assigned. 204 | */ 205 | public Optional parse(String source) throws IllegalStateException { 206 | return parse(source, InputEncoding.UTF_8); 207 | } 208 | 209 | /** 210 | * Parse source code from a string and create a syntax tree. 211 | * 212 | * @return An optional {@linkplain Tree} which is empty if parsing was halted. 213 | * @throws IllegalStateException If the parser does not have a language assigned. 214 | */ 215 | public Optional parse(String source, InputEncoding encoding) throws IllegalStateException { 216 | return parse(source, encoding, null); 217 | } 218 | 219 | /** 220 | * Parse source code from a string and create a syntax tree. 221 | * 222 | *

If you have already parsed an earlier version of this document and the 223 | * document has since been edited, pass the previous syntax tree to {@code oldTree} 224 | * so that the unchanged parts of it can be reused. This will save time and memory. 225 | *
For this to work correctly, you must have already edited the old syntax tree using 226 | * the {@link Tree#edit} method in a way that exactly matches the source code changes. 227 | * 228 | * @return An optional {@linkplain Tree} which is empty if parsing was halted. 229 | * @throws IllegalStateException If the parser does not have a language assigned. 230 | */ 231 | public Optional parse(String source, Tree oldTree) throws IllegalStateException { 232 | return parse(source, InputEncoding.UTF_8, oldTree); 233 | } 234 | 235 | /** 236 | * Parse source code from a string and create a syntax tree. 237 | * 238 | *

If you have already parsed an earlier version of this document and the 239 | * document has since been edited, pass the previous syntax tree to {@code oldTree} 240 | * so that the unchanged parts of it can be reused. This will save time and memory. 241 | *
For this to work correctly, you must have already edited the old syntax tree using 242 | * the {@link Tree#edit} method in a way that exactly matches the source code changes. 243 | * 244 | * @return An optional {@linkplain Tree} which is empty if parsing was halted. 245 | * @throws IllegalStateException If the parser does not have a language assigned. 246 | */ 247 | public Optional parse(String source, InputEncoding encoding, @Nullable Tree oldTree) 248 | throws IllegalStateException { 249 | if (language == null) { 250 | throw new IllegalStateException("The parser has no language assigned"); 251 | } 252 | 253 | try (var alloc = Arena.ofShared()) { 254 | var bytes = source.getBytes(encoding.charset()); 255 | var string = alloc.allocateFrom(C_CHAR, bytes); 256 | var old = oldTree == null ? MemorySegment.NULL : oldTree.segment(); 257 | var tree = ts_parser_parse_string_encoding(self, old, string, bytes.length, encoding.ordinal()); 258 | if (tree.equals(MemorySegment.NULL)) return Optional.empty(); 259 | return Optional.of(new Tree(tree, language, source, encoding.charset())); 260 | } 261 | } 262 | 263 | /** 264 | * Parse source code from a callback and create a syntax tree. 265 | * 266 | * @return An optional {@linkplain Tree} which is empty if parsing was halted. 267 | * @throws IllegalStateException If the parser does not have a language assigned. 268 | */ 269 | public Optional parse(ParseCallback parseCallback, InputEncoding encoding) throws IllegalStateException { 270 | return parse(parseCallback, encoding, null, null); 271 | } 272 | 273 | /** 274 | * Parse source code from a callback and create a syntax tree. 275 | * 276 | * @return An optional {@linkplain Tree} which is empty if parsing was halted. 277 | * @throws IllegalStateException If the parser does not have a language assigned. 278 | */ 279 | public Optional parse(ParseCallback parseCallback, InputEncoding encoding, Options options) 280 | throws IllegalStateException { 281 | return parse(parseCallback, encoding, null, options); 282 | } 283 | 284 | /** 285 | * Parse source code from a callback and create a syntax tree. 286 | * 287 | *

If you have already parsed an earlier version of this document and the 288 | * document has since been edited, pass the previous syntax tree to {@code oldTree} 289 | * so that the unchanged parts of it can be reused. This will save time and memory. 290 | *
For this to work correctly, you must have already edited the old syntax tree using 291 | * the {@link Tree#edit} method in a way that exactly matches the source code changes. 292 | * 293 | * @return An optional {@linkplain Tree} which is empty if parsing was halted. 294 | * @throws IllegalStateException If the parser does not have a language assigned. 295 | */ 296 | @SuppressWarnings("unused") 297 | public Optional parse( 298 | ParseCallback parseCallback, InputEncoding encoding, @Nullable Tree oldTree, @Nullable Options options) 299 | throws IllegalStateException { 300 | if (language == null) { 301 | throw new IllegalStateException("The parser has no language assigned"); 302 | } 303 | 304 | var input = TSInput.allocate(arena); 305 | TSInput.payload(input, MemorySegment.NULL); 306 | TSInput.encoding(input, encoding.ordinal()); 307 | // NOTE: can't use _ because of palantir/palantir-java-format#934 308 | var read = TSInput.read.allocate( 309 | (payload, index, point, bytes) -> { 310 | var result = parseCallback.apply(index, Point.from(point)); 311 | if (result == null) { 312 | bytes.set(C_INT, 0, 0); 313 | return MemorySegment.NULL; 314 | } 315 | var buffer = result.getBytes(encoding.charset()); 316 | bytes.set(C_INT, 0, buffer.length); 317 | return arena.allocateFrom(C_CHAR, buffer); 318 | }, 319 | arena); 320 | TSInput.read(input, read); 321 | 322 | MemorySegment tree, old = oldTree == null ? MemorySegment.NULL : oldTree.segment(); 323 | if (options == null) { 324 | tree = ts_parser_parse(self, old, input); 325 | } else { 326 | var parseOptions = TSParseOptions.allocate(arena); 327 | TSParseOptions.payload(parseOptions, MemorySegment.NULL); 328 | var progress = TSParseOptions.progress_callback.allocate( 329 | (payload) -> { 330 | var offset = TSParseState.current_byte_offset(payload); 331 | var hasError = TSParseState.has_error(payload); 332 | return options.progressCallback(new State(offset, hasError)); 333 | }, 334 | arena); 335 | TSParseOptions.progress_callback(parseOptions, progress); 336 | tree = ts_parser_parse_with_options(self, old, input, parseOptions); 337 | } 338 | if (tree.equals(MemorySegment.NULL)) return Optional.empty(); 339 | return Optional.of(new Tree(tree, language, null, null)); 340 | } 341 | 342 | /** 343 | * Instruct the parser to start the next {@linkplain #parse parse} from the beginning. 344 | * 345 | * @apiNote If parsing was previously halted, the parser will resume where it left off. 346 | * If you intend to parse another document instead, you must call this method first. 347 | */ 348 | public void reset() { 349 | ts_parser_reset(self); 350 | } 351 | 352 | @Override 353 | public void close() throws RuntimeException { 354 | arena.close(); 355 | } 356 | 357 | @Override 358 | public String toString() { 359 | return "Parser{language=%s}".formatted(language); 360 | } 361 | 362 | /** 363 | * A class representing the current state of the parser. 364 | * 365 | * @since 0.25.0 366 | */ 367 | public static final class State { 368 | private final @Unsigned int currentByteOffset; 369 | private final boolean hasError; 370 | 371 | private State(@Unsigned int currentByteOffset, boolean hasError) { 372 | this.currentByteOffset = currentByteOffset; 373 | this.hasError = hasError; 374 | } 375 | 376 | /** Get the current byte offset of the parser. */ 377 | public @Unsigned int getCurrentByteOffset() { 378 | return currentByteOffset; 379 | } 380 | 381 | /** Check if the parser has encountered an error. */ 382 | public boolean hasError() { 383 | return hasError; 384 | } 385 | 386 | @Override 387 | public String toString() { 388 | return String.format( 389 | "Parser.State{currentByteOffset=%s, hasError=%s}", 390 | Integer.toUnsignedString(currentByteOffset), hasError); 391 | } 392 | } 393 | 394 | /** 395 | * A class representing the parser options. 396 | * 397 | * @since 0.25.0 398 | */ 399 | @NullMarked 400 | public static final class Options { 401 | private final Predicate progressCallback; 402 | 403 | public Options(Predicate progressCallback) { 404 | this.progressCallback = progressCallback; 405 | } 406 | 407 | private boolean progressCallback(State state) { 408 | return progressCallback.test(state); 409 | } 410 | } 411 | 412 | /** 413 | * A class representing a cancellation flag. 414 | * 415 | * @deprecated Use {@link Options} instead. 416 | */ 417 | @Deprecated(since = "0.25.0") 418 | @SuppressWarnings("DeprecatedIsStillUsed") 419 | public static class CancellationFlag { 420 | private final Arena arena = Arena.ofAuto(); 421 | private final MemorySegment segment = arena.allocate(C_LONG_LONG); 422 | private final AtomicLong value = new AtomicLong(); 423 | 424 | /** Creates an uninitialized cancellation flag. */ 425 | public CancellationFlag() {} 426 | 427 | /** Get the value of the flag. */ 428 | public long get() { 429 | return value.get(); 430 | } 431 | 432 | /** Set the value of the flag. */ 433 | @SuppressWarnings("unused") 434 | public void set(long value) { 435 | // NOTE: can't use _ because of palantir/palantir-java-format#934 436 | segment.set(C_LONG_LONG, 0L, this.value.updateAndGet(o -> value)); 437 | } 438 | } 439 | } 440 | -------------------------------------------------------------------------------- /src/main/java/io/github/treesitter/jtreesitter/Point.java: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.jtreesitter; 2 | 3 | import io.github.treesitter.jtreesitter.internal.TSPoint; 4 | import java.lang.foreign.MemorySegment; 5 | import java.lang.foreign.SegmentAllocator; 6 | 7 | /** 8 | * A position in a text document in terms of rows and columns. 9 | * 10 | * @param row The zero-based row of the document. 11 | * @param column The zero-based column of the document. 12 | */ 13 | public record Point(@Unsigned int row, @Unsigned int column) implements Comparable { 14 | /** The minimum value a {@linkplain Point} can have. */ 15 | public static final Point MIN = new Point(0, 0); 16 | 17 | /** The maximum value a {@linkplain Point} can have. */ 18 | public static final Point MAX = new Point(-1, -1); 19 | 20 | static Point from(MemorySegment point) { 21 | return new Point(TSPoint.row(point), TSPoint.column(point)); 22 | } 23 | 24 | MemorySegment into(SegmentAllocator allocator) { 25 | var point = TSPoint.allocate(allocator); 26 | TSPoint.row(point, row); 27 | TSPoint.column(point, column); 28 | return point; 29 | } 30 | 31 | @Override 32 | public int compareTo(Point other) { 33 | var rowDiff = Integer.compareUnsigned(row, other.row); 34 | if (rowDiff != 0) return rowDiff; 35 | return Integer.compareUnsigned(column, other.column); 36 | } 37 | 38 | @Override 39 | public String toString() { 40 | return "Point[row=%s, column=%s]".formatted(Integer.toUnsignedString(row), Integer.toUnsignedString(column)); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/io/github/treesitter/jtreesitter/QueryCapture.java: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.jtreesitter; 2 | 3 | import org.jspecify.annotations.NullMarked; 4 | 5 | /** 6 | * A {@link Node} that was captured with a certain capture name. 7 | * 8 | * @param name The name of the capture. 9 | * @param node The captured node. 10 | */ 11 | @NullMarked 12 | public record QueryCapture(String name, Node node) {} 13 | -------------------------------------------------------------------------------- /src/main/java/io/github/treesitter/jtreesitter/QueryCursor.java: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.jtreesitter; 2 | 3 | import static io.github.treesitter.jtreesitter.internal.TreeSitter.*; 4 | 5 | import io.github.treesitter.jtreesitter.internal.*; 6 | import java.lang.foreign.Arena; 7 | import java.lang.foreign.MemorySegment; 8 | import java.lang.foreign.SegmentAllocator; 9 | import java.util.AbstractMap.SimpleImmutableEntry; 10 | import java.util.function.BiPredicate; 11 | import java.util.function.Predicate; 12 | import java.util.stream.Stream; 13 | import java.util.stream.StreamSupport; 14 | import org.jspecify.annotations.NullMarked; 15 | import org.jspecify.annotations.Nullable; 16 | 17 | /** 18 | * A class that can be used to execute a {@linkplain Query query} 19 | * on a {@linkplain Tree syntax tree}. 20 | * 21 | * @since 0.25.0 22 | */ 23 | @NullMarked 24 | public class QueryCursor implements AutoCloseable { 25 | private final MemorySegment self; 26 | private final Arena arena; 27 | private final Query query; 28 | 29 | /** Create a new cursor for the given query. */ 30 | public QueryCursor(Query query) { 31 | this.query = query; 32 | arena = Arena.ofShared(); 33 | self = ts_query_cursor_new().reinterpret(arena, TreeSitter::ts_query_cursor_delete); 34 | } 35 | 36 | /** 37 | * Get the maximum number of in-progress matches. 38 | * 39 | * @apiNote Defaults to {@code -1} (unlimited). 40 | */ 41 | public @Unsigned int getMatchLimit() { 42 | return ts_query_cursor_match_limit(self); 43 | } 44 | 45 | /** 46 | * Get the maximum number of in-progress matches. 47 | * 48 | * @throws IllegalArgumentException If {@code matchLimit == 0}. 49 | */ 50 | public QueryCursor setMatchLimit(@Unsigned int matchLimit) throws IllegalArgumentException { 51 | if (matchLimit == 0) { 52 | throw new IllegalArgumentException("The match limit cannot equal 0"); 53 | } 54 | ts_query_cursor_set_match_limit(self, matchLimit); 55 | return this; 56 | } 57 | 58 | /** 59 | * Get the maximum duration in microseconds that query 60 | * execution should be allowed to take before halting. 61 | * 62 | * @apiNote Defaults to {@code 0} (unlimited). 63 | * 64 | * @deprecated Use {@link Options} instead. 65 | */ 66 | @Deprecated(since = "0.25.0") 67 | public @Unsigned long getTimeoutMicros() { 68 | return ts_query_cursor_timeout_micros(self); 69 | } 70 | 71 | /** 72 | * Set the maximum duration in microseconds that query 73 | * execution should be allowed to take before halting. 74 | * 75 | * @deprecated Use {@link Options} instead. 76 | */ 77 | @Deprecated(since = "0.25.0") 78 | public QueryCursor setTimeoutMicros(@Unsigned long timeoutMicros) { 79 | ts_query_cursor_set_timeout_micros(self, timeoutMicros); 80 | return this; 81 | } 82 | 83 | /** 84 | * Set the maximum start depth for the query. 85 | * 86 | *

This prevents cursors from exploring children nodes at a certain depth. 87 | *
Note that if a pattern includes many children, then they will still be checked. 88 | */ 89 | public QueryCursor setMaxStartDepth(@Unsigned int maxStartDepth) { 90 | ts_query_cursor_set_max_start_depth(self, maxStartDepth); 91 | return this; 92 | } 93 | 94 | /** 95 | * Set the range of bytes in which the query will be executed. 96 | *

The query cursor will return matches that intersect with the given range. 97 | * This means that a match may be returned even if some of its captures fall 98 | * outside the specified range, as long as at least part of the match 99 | * overlaps with the range. 100 | * 101 | *

For example, if a query pattern matches a node that spans a larger area 102 | * than the specified range, but part of that node intersects with the range, 103 | * the entire match will be returned. 104 | * 105 | * @throws IllegalArgumentException If `endByte > startByte`. 106 | */ 107 | public QueryCursor setByteRange(@Unsigned int startByte, @Unsigned int endByte) throws IllegalArgumentException { 108 | if (!ts_query_cursor_set_byte_range(self, startByte, endByte)) { 109 | throw new IllegalArgumentException("Invalid byte range"); 110 | } 111 | return this; 112 | } 113 | 114 | /** 115 | * Set the range of points in which the query will be executed. 116 | * 117 | *

The query cursor will return matches that intersect with the given range. 118 | * This means that a match may be returned even if some of its captures fall 119 | * outside the specified range, as long as at least part of the match 120 | * overlaps with the range. 121 | * 122 | *

For example, if a query pattern matches a node that spans a larger area 123 | * than the specified range, but part of that node intersects with the range, 124 | * the entire match will be returned. 125 | * 126 | * @throws IllegalArgumentException If `endPoint > startPoint`. 127 | */ 128 | public QueryCursor setPointRange(Point startPoint, Point endPoint) throws IllegalArgumentException { 129 | try (var alloc = Arena.ofConfined()) { 130 | MemorySegment start = startPoint.into(alloc), end = endPoint.into(alloc); 131 | if (!ts_query_cursor_set_point_range(self, start, end)) { 132 | throw new IllegalArgumentException("Invalid point range"); 133 | } 134 | } 135 | return this; 136 | } 137 | 138 | /** 139 | * Check if the query exceeded its maximum number of 140 | * in-progress matches during its last execution. 141 | */ 142 | public boolean didExceedMatchLimit() { 143 | return ts_query_cursor_did_exceed_match_limit(self); 144 | } 145 | 146 | private void exec(Node node, @Nullable Options options) { 147 | try (var alloc = Arena.ofConfined()) { 148 | if (options == null || options.progressCallback == null) { 149 | ts_query_cursor_exec(self, query.segment(), node.copy(alloc)); 150 | } else { 151 | var cursorOptions = TSQueryCursorOptions.allocate(alloc); 152 | TSQueryCursorOptions.payload(cursorOptions, MemorySegment.NULL); 153 | var progress = TSQueryCursorOptions.progress_callback.allocate( 154 | (payload) -> { 155 | var offset = TSQueryCursorState.current_byte_offset(payload); 156 | return options.progressCallback.test(new State(offset)); 157 | }, 158 | alloc); 159 | TSQueryCursorOptions.progress_callback(cursorOptions, progress); 160 | ts_query_cursor_exec_with_options(self, query.segment(), node.copy(alloc), cursorOptions); 161 | } 162 | } 163 | } 164 | 165 | /** 166 | * Iterate over all the captures in the order that they were found. 167 | * 168 | *

This is useful if you don't care about which pattern matched, 169 | * and just want a single, ordered sequence of captures. 170 | * 171 | * @param node The node that the query will run on. 172 | * 173 | * @implNote The lifetime of the matches is bound to that of the cursor. 174 | */ 175 | public Stream> findCaptures(Node node) { 176 | return findCaptures(node, arena, new Options(null, null)); 177 | } 178 | 179 | /** 180 | * Iterate over all the captures in the order that they were found. 181 | * 182 | *

This is useful if you don't care about which pattern matched, 183 | * and just want a single, ordered sequence of captures. 184 | * 185 | * @param node The node that the query will run on. 186 | * 187 | * @implNote The lifetime of the matches is bound to that of the cursor. 188 | */ 189 | public Stream> findCaptures(Node node, Options options) { 190 | return findCaptures(node, arena, options); 191 | } 192 | 193 | /** 194 | * Iterate over all the captures in the order that they were found. 195 | * 196 | *

This is useful if you don't care about which pattern matched, 197 | * and just want a single, ordered sequence of captures. 198 | * 199 | * @param node The node that the query will run on. 200 | */ 201 | public Stream> findCaptures( 202 | Node node, SegmentAllocator allocator, Options options) { 203 | exec(node, options); 204 | var iterator = new CapturesIterator(query, self, node.getTree(), allocator, options.predicateCallback); 205 | return StreamSupport.stream(iterator, false); 206 | } 207 | 208 | /** 209 | * Iterate over all the matches in the order that they were found. 210 | * 211 | *

Because multiple patterns can match the same set of nodes, one match may contain 212 | * captures that appear before some of the captures from a previous match. 213 | * 214 | * @param node The node that the query will run on. 215 | * 216 | * @implNote The lifetime of the matches is bound to that of the cursor. 217 | */ 218 | public Stream findMatches(Node node) { 219 | return findMatches(node, arena, new Options(null, null)); 220 | } 221 | 222 | /** 223 | * Iterate over all the matches in the order that they were found. 224 | * 225 | *

Because multiple patterns can match the same set of nodes, one match may contain 226 | * captures that appear before some of the captures from a previous match. 227 | * 228 | *

Predicate Example

229 | *

230 | * {@snippet lang = "java": 231 | * QueryCursor.Options options = new QueryCursor.Options((predicate, match) -> { 232 | * if (!predicate.getName().equals("ieq?")) return true; 233 | * List args = predicate.getArgs(); 234 | * Node node = match.findNodes(args.getFirst().value()).getFirst(); 235 | * return args.getLast().value().equalsIgnoreCase(node.getText()); 236 | * }); 237 | * Stream matches = self.findMatches(tree.getRootNode(), options); 238 | *} 239 | * 240 | * @param node The node that the query will run on. 241 | * 242 | * @implNote The lifetime of the matches is bound to that of the cursor. 243 | */ 244 | public Stream findMatches(Node node, Options options) { 245 | return findMatches(node, arena, options); 246 | } 247 | 248 | /** 249 | * Iterate over all the matches in the order that they were found, using the given allocator. 250 | * 251 | *

Because multiple patterns can match the same set of nodes, one match may contain 252 | * captures that appear before some of the captures from a previous match. 253 | * 254 | * @param node The node that the query will run on. 255 | * 256 | * @see #findMatches(Node, Options) 257 | */ 258 | public Stream findMatches(Node node, SegmentAllocator allocator, Options options) { 259 | exec(node, options); 260 | var iterator = new MatchesIterator(query, self, node.getTree(), allocator, options.predicateCallback); 261 | return StreamSupport.stream(iterator, false); 262 | } 263 | 264 | @Override 265 | public void close() throws RuntimeException { 266 | arena.close(); 267 | } 268 | 269 | /** A class representing the current state of the query cursor. */ 270 | public static final class State { 271 | private final @Unsigned int currentByteOffset; 272 | 273 | private State(@Unsigned int currentByteOffset) { 274 | this.currentByteOffset = currentByteOffset; 275 | } 276 | 277 | /** Get the current byte offset of the cursor. */ 278 | public @Unsigned int getCurrentByteOffset() { 279 | return currentByteOffset; 280 | } 281 | 282 | @Override 283 | public String toString() { 284 | return String.format( 285 | "QueryCursor.State{currentByteOffset=%s}", Integer.toUnsignedString(currentByteOffset)); 286 | } 287 | } 288 | 289 | /** A class representing the query cursor options. */ 290 | @NullMarked 291 | public static class Options { 292 | private final @Nullable Predicate progressCallback; 293 | private final @Nullable BiPredicate predicateCallback; 294 | 295 | /** 296 | * @param progressCallback Progress handler. 297 | * @param predicateCallback Custom predicate handler. 298 | */ 299 | private Options( 300 | @Nullable Predicate progressCallback, 301 | @Nullable BiPredicate predicateCallback) { 302 | this.progressCallback = progressCallback; 303 | this.predicateCallback = predicateCallback; 304 | } 305 | 306 | /** 307 | * @param progressCallback Progress handler. 308 | */ 309 | public Options(Predicate progressCallback) { 310 | this.progressCallback = progressCallback; 311 | this.predicateCallback = null; 312 | } 313 | 314 | /** 315 | * @param predicateCallback Custom predicate handler. 316 | */ 317 | public Options(BiPredicate predicateCallback) { 318 | this.progressCallback = null; 319 | this.predicateCallback = predicateCallback; 320 | } 321 | } 322 | } 323 | -------------------------------------------------------------------------------- /src/main/java/io/github/treesitter/jtreesitter/QueryError.java: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.jtreesitter; 2 | 3 | import org.jspecify.annotations.NonNull; 4 | 5 | /** Any error that occurred while instantiating a {@link Query}. */ 6 | public abstract sealed class QueryError extends IllegalArgumentException 7 | permits QueryError.Capture, 8 | QueryError.Field, 9 | QueryError.NodeType, 10 | QueryError.Structure, 11 | QueryError.Syntax, 12 | QueryError.Predicate { 13 | 14 | protected QueryError(@NonNull String message, Throwable cause) { 15 | super(message, cause); 16 | } 17 | 18 | protected QueryError(@NonNull String message) { 19 | super(message, null); 20 | } 21 | 22 | /** A query syntax error. */ 23 | public static final class Syntax extends QueryError { 24 | Syntax() { 25 | super("Unexpected EOF"); 26 | } 27 | 28 | Syntax(long row, long column) { 29 | super("Invalid syntax at row %d, column %d".formatted(row, column)); 30 | } 31 | } 32 | 33 | /** A capture name error. */ 34 | public static final class Capture extends QueryError { 35 | Capture(long row, long column, @NonNull CharSequence capture) { 36 | super("Invalid capture name at row %d, column %d: %s".formatted(row, column, capture)); 37 | } 38 | } 39 | 40 | /** A field name error. */ 41 | public static final class Field extends QueryError { 42 | Field(long row, long column, @NonNull CharSequence field) { 43 | super("Invalid field name at row %d, column %d: %s".formatted(row, column, field)); 44 | } 45 | } 46 | 47 | /** A node type error. */ 48 | public static final class NodeType extends QueryError { 49 | NodeType(long row, long column, @NonNull CharSequence type) { 50 | super("Invalid node type at row %d, column %d: %s".formatted(row, column, type)); 51 | } 52 | } 53 | 54 | /** A pattern structure error. */ 55 | public static final class Structure extends QueryError { 56 | Structure(long row, long column) { 57 | super("Impossible pattern at row %d, column %d".formatted(row, column)); 58 | } 59 | } 60 | 61 | /** A query predicate error. */ 62 | public static final class Predicate extends QueryError { 63 | Predicate(long row, @NonNull String details, Throwable cause) { 64 | super("Invalid predicate in pattern at row %d: %s".formatted(row, details), cause); 65 | } 66 | 67 | Predicate(long row, @NonNull String format, Object... args) { 68 | this(row, String.format(format, args), (Throwable) null); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/io/github/treesitter/jtreesitter/QueryMatch.java: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.jtreesitter; 2 | 3 | import io.github.treesitter.jtreesitter.internal.*; 4 | import java.lang.foreign.MemorySegment; 5 | import java.lang.foreign.SegmentAllocator; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import org.jspecify.annotations.NullMarked; 9 | 10 | /** A match that corresponds to a certain pattern in the query. */ 11 | @NullMarked 12 | public record QueryMatch(@Unsigned int patternIndex, List captures) { 13 | /** Creates an instance of a QueryMatch record class. */ 14 | public QueryMatch(@Unsigned int patternIndex, List captures) { 15 | this.patternIndex = patternIndex; 16 | this.captures = List.copyOf(captures); 17 | } 18 | 19 | static QueryMatch from(MemorySegment match, List captureNames, Tree tree, SegmentAllocator allocator) { 20 | var count = Short.toUnsignedInt(TSQueryMatch.capture_count(match)); 21 | var matchCaptures = TSQueryMatch.captures(match); 22 | var captureList = new ArrayList(count); 23 | for (int i = 0; i < count; ++i) { 24 | var capture = TSQueryCapture.asSlice(matchCaptures, i); 25 | var name = captureNames.get(TSQueryCapture.index(capture)); 26 | var node = TSNode.allocate(allocator).copyFrom(TSQueryCapture.node(capture)); 27 | captureList.add(new QueryCapture(name, new Node(node, tree))); 28 | } 29 | var patternIndex = TSQueryMatch.pattern_index(match); 30 | return new QueryMatch(patternIndex, captureList); 31 | } 32 | 33 | /** Find the nodes that are captured by the given capture name. */ 34 | public List findNodes(String capture) { 35 | return captures.stream() 36 | .filter(c -> c.name().equals(capture)) 37 | .map(QueryCapture::node) 38 | .toList(); 39 | } 40 | 41 | @Override 42 | public String toString() { 43 | return String.format( 44 | "QueryMatch[patternIndex=%s, captures=%s]", Integer.toUnsignedString(patternIndex), captures); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/io/github/treesitter/jtreesitter/QueryPredicate.java: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.jtreesitter; 2 | 3 | import java.util.*; 4 | import java.util.function.Predicate; 5 | import java.util.regex.Pattern; 6 | import org.jspecify.annotations.NullMarked; 7 | 8 | /** 9 | * A query predicate that associates conditions (or arbitrary metadata) with a pattern. 10 | * 11 | * @see Predicates 12 | */ 13 | @NullMarked 14 | public sealed class QueryPredicate permits QueryPredicate.AnyOf, QueryPredicate.Eq, QueryPredicate.Match { 15 | private final String name; 16 | protected final List args; 17 | 18 | protected QueryPredicate(String name, int argc) { 19 | this(name, new ArrayList<>(argc)); 20 | } 21 | 22 | QueryPredicate(String name, List args) { 23 | this.name = name; 24 | this.args = args; 25 | } 26 | 27 | /** Get the name of the predicate. */ 28 | public String getName() { 29 | return name; 30 | } 31 | 32 | /** Get the arguments given to the predicate. */ 33 | public List getArgs() { 34 | return Collections.unmodifiableList(args); 35 | } 36 | 37 | boolean test(QueryMatch queryMatch) { 38 | return true; 39 | } 40 | 41 | @Override 42 | public String toString() { 43 | return "QueryPredicate{name=%s, args=%s}".formatted(name, args); 44 | } 45 | 46 | /** 47 | * Handles the following predicates:
48 | * {@code #eq?}, {@code #not-eq?}, {@code #any-eq?}, {@code #any-not-eq?} 49 | */ 50 | @NullMarked 51 | public static final class Eq extends QueryPredicate { 52 | private final String capture; 53 | private final String value; 54 | private final boolean isPositive; 55 | private final boolean isAny; 56 | private final boolean isCapture; 57 | 58 | static final Set NAMES = Set.of("eq?", "not-eq?", "any-eq?", "any-not-eq?"); 59 | 60 | Eq(String name, String capture, String value, boolean isCapture) { 61 | super(name, 2); 62 | this.capture = capture; 63 | this.value = value; 64 | this.isPositive = !name.contains("not-"); 65 | this.isAny = name.startsWith("any-"); 66 | this.isCapture = isCapture; 67 | 68 | args.add(new QueryPredicateArg.Capture(capture)); 69 | if (isCapture) args.add(new QueryPredicateArg.Capture(value)); 70 | else args.add(new QueryPredicateArg.Literal(value)); 71 | } 72 | 73 | @Override 74 | boolean test(QueryMatch match) { 75 | return isCapture ? testCapture(match) : testLiteral(match); 76 | } 77 | 78 | private boolean testCapture(QueryMatch match) { 79 | var findNodes1 = match.findNodes(capture).stream(); 80 | var findNodes2 = match.findNodes(value).stream(); 81 | Predicate predicate = 82 | n1 -> findNodes2.anyMatch(n2 -> Objects.equals(n1.getText(), n2.getText()) == isPositive); 83 | return isAny ? findNodes1.anyMatch(predicate) : findNodes1.allMatch(predicate); 84 | } 85 | 86 | private boolean testLiteral(QueryMatch match) { 87 | var findNodes1 = match.findNodes(capture); 88 | if (findNodes1.isEmpty()) return !isPositive; 89 | Predicate predicate = node -> { 90 | var text = Objects.requireNonNull(node.getText()); 91 | return value.equals(text) == isPositive; 92 | }; 93 | if (!isAny) return findNodes1.stream().allMatch(predicate); 94 | return findNodes1.stream().anyMatch(predicate); 95 | } 96 | } 97 | 98 | /** 99 | * Handles the following predicates:
100 | * {@code #match?}, {@code #not-match?}, {@code #any-match?}, {@code #any-not-match?} 101 | */ 102 | @NullMarked 103 | public static final class Match extends QueryPredicate { 104 | private final String capture; 105 | private final Pattern pattern; 106 | private final boolean isPositive; 107 | private final boolean isAny; 108 | 109 | static final Set NAMES = Set.of("match?", "not-match?", "any-match?", "any-not-match?"); 110 | 111 | Match(String name, String capture, Pattern pattern) { 112 | super(name, 2); 113 | this.capture = capture; 114 | this.pattern = pattern; 115 | this.isPositive = !name.contains("not-"); 116 | this.isAny = name.startsWith("any-"); 117 | 118 | args.add(new QueryPredicateArg.Capture(capture)); 119 | args.add(new QueryPredicateArg.Literal(pattern.pattern())); 120 | } 121 | 122 | @Override 123 | boolean test(QueryMatch match) { 124 | var findNodes1 = match.findNodes(capture); 125 | if (findNodes1.isEmpty()) return !isPositive; 126 | Predicate predicate = node -> { 127 | var text = Objects.requireNonNull(node.getText()); 128 | return pattern.matcher(text).hasMatch() == isPositive; 129 | }; 130 | if (!isAny) return findNodes1.stream().allMatch(predicate); 131 | return findNodes1.stream().anyMatch(predicate); 132 | } 133 | } 134 | 135 | /** 136 | * Handles the following predicates:
137 | * {@code #any-of?}, {@code #not-any-of?} 138 | */ 139 | @NullMarked 140 | public static final class AnyOf extends QueryPredicate { 141 | private final String capture; 142 | private final List values; 143 | private final boolean isPositive; 144 | 145 | static final Set NAMES = Set.of("any-of?", "not-any-of?"); 146 | 147 | AnyOf(String name, String capture, List values) { 148 | super(name, values.size() + 1); 149 | this.capture = capture; 150 | this.values = List.copyOf(values); 151 | this.isPositive = name.equals("any-of?"); 152 | 153 | args.add(new QueryPredicateArg.Capture(capture)); 154 | for (var value : this.values) { 155 | args.add(new QueryPredicateArg.Literal(value)); 156 | } 157 | } 158 | 159 | @Override 160 | boolean test(QueryMatch match) { 161 | return match.findNodes(capture).stream().noneMatch(node -> { 162 | var text = Objects.requireNonNull(node.getText()); 163 | return values.contains(text) != isPositive; 164 | }); 165 | } 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/main/java/io/github/treesitter/jtreesitter/QueryPredicateArg.java: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.jtreesitter; 2 | 3 | import org.jspecify.annotations.NullMarked; 4 | 5 | /** An argument to a {@link QueryPredicate}. */ 6 | @NullMarked 7 | public sealed interface QueryPredicateArg permits QueryPredicateArg.Capture, QueryPredicateArg.Literal { 8 | /** The value of the argument. */ 9 | String value(); 10 | 11 | /** A capture argument ({@code @value}). */ 12 | record Capture(String value) implements QueryPredicateArg { 13 | @Override 14 | public String toString() { 15 | return "@%s".formatted(value); 16 | } 17 | } 18 | 19 | /** A literal string argument ({@code "value"}). */ 20 | record Literal(String value) implements QueryPredicateArg { 21 | @Override 22 | public String toString() { 23 | return "\"%s\"".formatted(value); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/io/github/treesitter/jtreesitter/Range.java: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.jtreesitter; 2 | 3 | import io.github.treesitter.jtreesitter.internal.TSRange; 4 | import java.lang.foreign.MemorySegment; 5 | import java.lang.foreign.SegmentAllocator; 6 | import org.jspecify.annotations.NullMarked; 7 | 8 | /** 9 | * A range of positions in a text document, 10 | * both in terms of bytes and of row-column points. 11 | */ 12 | @NullMarked 13 | public record Range(Point startPoint, Point endPoint, @Unsigned int startByte, @Unsigned int endByte) { 14 | static final Range DEFAULT = new Range(Point.MIN, Point.MAX, 0, -1); 15 | 16 | /** 17 | * Creates an instance of a Range record class. 18 | * 19 | * @throws IllegalArgumentException If {@code startPoint > endPoint} or {@code startByte > endByte}. 20 | */ 21 | public Range { 22 | if (startPoint.compareTo(endPoint) > 0) { 23 | throw new IllegalArgumentException("Invalid point range: %s to %s".formatted(startPoint, endPoint)); 24 | } 25 | if (Integer.compareUnsigned(startByte, endByte) > 0) { 26 | throw new IllegalArgumentException(String.format( 27 | "Invalid byte range: %s to %s", 28 | Integer.toUnsignedString(startByte), Integer.toUnsignedString(endByte))); 29 | } 30 | } 31 | 32 | static Range from(MemorySegment range) { 33 | int endByte = TSRange.end_byte(range), startByte = TSRange.start_byte(range); 34 | MemorySegment startPoint = TSRange.start_point(range), endPoint = TSRange.end_point(range); 35 | return new Range(Point.from(startPoint), Point.from(endPoint), startByte, endByte); 36 | } 37 | 38 | MemorySegment into(SegmentAllocator allocator) { 39 | var range = TSRange.allocate(allocator); 40 | TSRange.start_byte(range, startByte); 41 | TSRange.end_byte(range, endByte); 42 | TSRange.start_point(range, startPoint.into(allocator)); 43 | TSRange.end_point(range, endPoint.into(allocator)); 44 | return range; 45 | } 46 | 47 | @Override 48 | public String toString() { 49 | return String.format( 50 | "Range[startPoint=%s, endPoint=%s, startByte=%s, endByte=%s]", 51 | startPoint, endPoint, Integer.toUnsignedString(startByte), Integer.toUnsignedString(endByte)); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/io/github/treesitter/jtreesitter/Tree.java: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.jtreesitter; 2 | 3 | import static io.github.treesitter.jtreesitter.internal.TreeSitter.*; 4 | 5 | import io.github.treesitter.jtreesitter.internal.TSRange; 6 | import io.github.treesitter.jtreesitter.internal.TreeSitter; 7 | import java.lang.foreign.*; 8 | import java.nio.charset.Charset; 9 | import java.util.ArrayList; 10 | import java.util.Collections; 11 | import java.util.List; 12 | import org.jspecify.annotations.NullMarked; 13 | import org.jspecify.annotations.Nullable; 14 | 15 | /** A class that represents a syntax tree. */ 16 | @NullMarked 17 | public final class Tree implements AutoCloseable, Cloneable { 18 | private final MemorySegment self; 19 | private byte[] source; 20 | private @Nullable Charset charset; 21 | private final Arena arena; 22 | private final Language language; 23 | private @Nullable List includedRanges; 24 | 25 | Tree(MemorySegment self, Language language, @Nullable String source, @Nullable Charset charset) { 26 | arena = Arena.ofShared(); 27 | this.self = self.reinterpret(arena, TreeSitter::ts_tree_delete); 28 | this.language = language; 29 | this.source = source != null && charset != null ? source.getBytes(charset) : new byte[0]; 30 | this.charset = charset; 31 | } 32 | 33 | private Tree(Tree tree) { 34 | var copy = ts_tree_copy(tree.self); 35 | arena = Arena.ofShared(); 36 | self = copy.reinterpret(arena, TreeSitter::ts_tree_delete); 37 | language = tree.language; 38 | source = tree.source; 39 | charset = tree.charset; 40 | includedRanges = tree.includedRanges; 41 | } 42 | 43 | MemorySegment segment() { 44 | return self; 45 | } 46 | 47 | @Nullable 48 | String getRegion(@Unsigned int start, @Unsigned int end) { 49 | var length = Math.min(end, source.length) - start; 50 | return charset != null ? new String(source, start, length, charset) : null; 51 | } 52 | 53 | /** Get the language that was used to parse the syntax tree. */ 54 | public Language getLanguage() { 55 | return language; 56 | } 57 | 58 | /** Get the source code of the syntax tree, if available. */ 59 | public @Nullable String getText() { 60 | return charset != null ? new String(source, charset) : null; 61 | } 62 | 63 | /** Get the root node of the syntax tree. */ 64 | public Node getRootNode() { 65 | return new Node(ts_tree_root_node(arena, self), this); 66 | } 67 | 68 | /** 69 | * Get the root node of the syntax tree, but with 70 | * its position shifted forward by the given offset. 71 | */ 72 | public @Nullable Node getRootNodeWithOffset(@Unsigned int bytes, Point extent) { 73 | try (var alloc = Arena.ofShared()) { 74 | var offsetExtent = extent.into(alloc); 75 | var node = ts_tree_root_node_with_offset(arena, self, bytes, offsetExtent); 76 | if (ts_node_is_null(node)) return null; 77 | return new Node(node, this); 78 | } 79 | } 80 | 81 | /** Get the included ranges of the syntax tree. */ 82 | public List getIncludedRanges() { 83 | if (includedRanges == null) { 84 | try (var alloc = Arena.ofConfined()) { 85 | var length = alloc.allocate(C_INT.byteSize(), C_INT.byteAlignment()); 86 | var ranges = ts_tree_included_ranges(self, length); 87 | int size = length.get(C_INT, 0); 88 | if (size == 0) return Collections.emptyList(); 89 | 90 | includedRanges = new ArrayList<>(size); 91 | for (int i = 0; i < size; ++i) { 92 | var range = TSRange.asSlice(ranges, i); 93 | includedRanges.add(Range.from(range)); 94 | } 95 | free(ranges); 96 | } 97 | } 98 | return Collections.unmodifiableList(includedRanges); 99 | } 100 | 101 | /** 102 | * Compare an old edited syntax tree to a new 103 | * syntax tree representing the same document. 104 | * 105 | *

For this to work correctly, this tree must have been 106 | * edited such that its ranges match up to the new tree. 107 | * 108 | * @return A list of ranges whose syntactic structure has changed. 109 | */ 110 | public List getChangedRanges(Tree newTree) { 111 | try (var alloc = Arena.ofConfined()) { 112 | var length = alloc.allocate(C_INT.byteSize(), C_INT.byteAlignment()); 113 | var ranges = ts_tree_get_changed_ranges(self, newTree.self, length); 114 | int size = length.get(C_INT, 0); 115 | if (size == 0) return Collections.emptyList(); 116 | 117 | var changedRanges = new ArrayList(size); 118 | for (int i = 0; i < size; ++i) { 119 | var range = TSRange.asSlice(ranges, i); 120 | changedRanges.add(Range.from(range)); 121 | } 122 | free(ranges); 123 | return changedRanges; 124 | } 125 | } 126 | 127 | /** 128 | * Edit the syntax tree to keep it in sync 129 | * with source code that has been modified. 130 | */ 131 | public void edit(InputEdit edit) { 132 | try (var alloc = Arena.ofConfined()) { 133 | ts_tree_edit(self, edit.into(alloc)); 134 | } finally { 135 | source = new byte[0]; 136 | charset = null; 137 | } 138 | } 139 | 140 | /** Create a new tree cursor starting from the root node of the tree. */ 141 | public TreeCursor walk() { 142 | return new TreeCursor(this); 143 | } 144 | 145 | /** 146 | * Create a shallow copy of the syntax tree. 147 | * 148 | * @implNote You need to clone a tree in order to use it on more than 149 | * one thread at a time, as {@linkplain Tree} objects are not thread safe. 150 | */ 151 | @Override 152 | @SuppressWarnings("MethodDoesntCallSuperMethod") 153 | public Tree clone() { 154 | return new Tree(this); 155 | } 156 | 157 | @Override 158 | public void close() throws RuntimeException { 159 | arena.close(); 160 | } 161 | 162 | @Override 163 | public String toString() { 164 | return "Tree{language=%s, source=%s}".formatted(language, source); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/main/java/io/github/treesitter/jtreesitter/TreeCursor.java: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.jtreesitter; 2 | 3 | import static io.github.treesitter.jtreesitter.internal.TreeSitter.*; 4 | 5 | import java.lang.foreign.Arena; 6 | import java.lang.foreign.MemorySegment; 7 | import java.lang.foreign.SegmentAllocator; 8 | import java.util.OptionalInt; 9 | import org.jspecify.annotations.NullMarked; 10 | import org.jspecify.annotations.Nullable; 11 | 12 | /** 13 | * A class that can be used to efficiently walk a {@linkplain Tree syntax tree}. 14 | * 15 | * @apiNote The node the cursor was constructed with is considered the 16 | * root of the cursor, and the cursor cannot walk outside this node. 17 | */ 18 | @NullMarked 19 | public final class TreeCursor implements AutoCloseable, Cloneable { 20 | private final MemorySegment self; 21 | private final Arena arena; 22 | private final Tree tree; 23 | private @Nullable Node node; 24 | 25 | TreeCursor(Node node, Tree tree) { 26 | arena = Arena.ofShared(); 27 | self = ts_tree_cursor_new(arena, node.copy(arena)); 28 | this.tree = tree; 29 | } 30 | 31 | TreeCursor(Tree tree) { 32 | arena = Arena.ofShared(); 33 | var node = ts_tree_root_node(arena, tree.segment()); 34 | self = ts_tree_cursor_new(arena, node); 35 | this.tree = tree; 36 | } 37 | 38 | private TreeCursor(TreeCursor cursor) { 39 | arena = Arena.ofShared(); 40 | self = ts_tree_cursor_copy(arena, cursor.self); 41 | tree = cursor.tree.clone(); 42 | node = cursor.node; 43 | } 44 | 45 | /** 46 | * Get the current node of the cursor. 47 | * 48 | * @implNote The node will become invalid once the cursor is closed. 49 | */ 50 | public Node getCurrentNode() { 51 | if (this.node == null) { 52 | var node = ts_tree_cursor_current_node(arena, self); 53 | this.node = new Node(node, tree); 54 | } 55 | return this.node; 56 | } 57 | 58 | /** 59 | * Get the current node of the cursor using the given allocator. 60 | * 61 | * @since 0.25.0 62 | */ 63 | public Node getCurrentNode(SegmentAllocator allocator) { 64 | var node = ts_tree_cursor_current_node(allocator, self); 65 | return new Node(node, tree); 66 | } 67 | 68 | /** 69 | * Get the depth of the cursor's current node relative to 70 | * the original node that the cursor was constructed with. 71 | */ 72 | public @Unsigned int getCurrentDepth() { 73 | return ts_tree_cursor_current_depth(self); 74 | } 75 | 76 | /** 77 | * Get the field ID of the tree cursor's current node, or {@code 0}. 78 | * 79 | * @see Node#getChildByFieldId 80 | * @see Language#getFieldIdForName 81 | */ 82 | public @Unsigned short getCurrentFieldId() { 83 | return ts_tree_cursor_current_field_id(self); 84 | } 85 | 86 | /** 87 | * Get the field name of the tree cursor's current node, or {@code null}. 88 | * 89 | * @see Node#getChildByFieldName 90 | */ 91 | public @Nullable String getCurrentFieldName() { 92 | var segment = ts_tree_cursor_current_field_name(self); 93 | return segment.equals(MemorySegment.NULL) ? null : segment.getString(0); 94 | } 95 | 96 | /** 97 | * Get the index of the cursor's current node out of the descendants 98 | * of the original node that the cursor was constructed with. 99 | */ 100 | public @Unsigned int getCurrentDescendantIndex() { 101 | return ts_tree_cursor_current_descendant_index(self); 102 | } 103 | 104 | /** 105 | * Move the cursor to the first child of its current node. 106 | * 107 | * @return {@code true} if the cursor successfully moved, or 108 | * {@code false} if there were no children. 109 | */ 110 | public boolean gotoFirstChild() { 111 | var result = ts_tree_cursor_goto_first_child(self); 112 | if (result) node = null; 113 | return result; 114 | } 115 | 116 | /** 117 | * Move the cursor to the last child of its current node. 118 | * 119 | * @return {@code true} if the cursor successfully moved, or 120 | * {@code false} if there were no children. 121 | */ 122 | public boolean gotoLastChild() { 123 | var result = ts_tree_cursor_goto_last_child(self); 124 | if (result) node = null; 125 | return result; 126 | } 127 | 128 | /** 129 | * Move the cursor to the parent of its current node. 130 | * 131 | * @return {@code true} if the cursor successfully moved, or 132 | * {@code false} if there was no parent node. 133 | */ 134 | public boolean gotoParent() { 135 | var result = ts_tree_cursor_goto_parent(self); 136 | if (result) node = null; 137 | return result; 138 | } 139 | 140 | /** 141 | * Move the cursor to the next sibling of its current node. 142 | * 143 | * @return {@code true} if the cursor successfully moved, or 144 | * {@code false} if there was no next sibling node. 145 | */ 146 | public boolean gotoNextSibling() { 147 | var result = ts_tree_cursor_goto_next_sibling(self); 148 | if (result) node = null; 149 | return result; 150 | } 151 | 152 | /** 153 | * Move the cursor to the previous sibling of its current node. 154 | * 155 | * @return {@code true} if the cursor successfully moved, or 156 | * {@code false} if there was no previous sibling node. 157 | */ 158 | public boolean gotoPreviousSibling() { 159 | var result = ts_tree_cursor_goto_previous_sibling(self); 160 | if (result) node = null; 161 | return result; 162 | } 163 | 164 | /** 165 | * Move the cursor to the node that is the nth descendant of 166 | * the original node that the cursor was constructed with. 167 | * 168 | * @apiNote The index {@code 0} represents the original node itself. 169 | */ 170 | public void gotoDescendant(@Unsigned int index) { 171 | ts_tree_cursor_goto_descendant(self, index); 172 | node = null; 173 | } 174 | 175 | /** 176 | * Move the cursor to the first child of its current node 177 | * that contains or starts after the given byte offset. 178 | * 179 | * @return The index of the child node, if found. 180 | */ 181 | public @Unsigned OptionalInt gotoFirstChildForByte(@Unsigned int offset) { 182 | var index = ts_tree_cursor_goto_first_child_for_byte(self, offset); 183 | if (index == -1L) return OptionalInt.empty(); 184 | node = null; 185 | return OptionalInt.of((int) index); 186 | } 187 | 188 | /** 189 | * Move the cursor to the first child of its current node 190 | * that contains or starts after the given point. 191 | * 192 | * @return The index of the child node, if found. 193 | */ 194 | public @Unsigned OptionalInt gotoFirstChildForPoint(Point point) { 195 | try (var arena = Arena.ofConfined()) { 196 | var goal = point.into(arena); 197 | var index = ts_tree_cursor_goto_first_child_for_point(self, goal); 198 | if (index == -1L) return OptionalInt.empty(); 199 | node = null; 200 | return OptionalInt.of((int) index); 201 | } 202 | } 203 | 204 | /** Reset the cursor to start at a different node. */ 205 | public void reset(Node node) { 206 | try (var arena = Arena.ofConfined()) { 207 | ts_tree_cursor_reset(self, node.copy(arena)); 208 | } finally { 209 | this.node = null; 210 | } 211 | } 212 | 213 | /** Reset the cursor to start at the same position as another cursor. */ 214 | public void reset(TreeCursor cursor) { 215 | ts_tree_cursor_reset_to(self, cursor.self); 216 | this.node = null; 217 | } 218 | 219 | /** Create a shallow copy of the tree cursor. */ 220 | @Override 221 | @SuppressWarnings("MethodDoesntCallSuperMethod") 222 | public TreeCursor clone() { 223 | return new TreeCursor(this); 224 | } 225 | 226 | @Override 227 | public void close() throws RuntimeException { 228 | ts_tree_cursor_delete(self); 229 | arena.close(); 230 | } 231 | 232 | @Override 233 | public String toString() { 234 | return "TreeCursor{tree=%s}".formatted(tree); 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/main/java/io/github/treesitter/jtreesitter/Unsigned.java: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.jtreesitter; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.ElementType; 5 | import java.lang.annotation.Target; 6 | 7 | /** 8 | * Specifies that the value is of an unsigned data type. 9 | * 10 | * @see Integer#compareUnsigned 11 | * @see Integer#toUnsignedString 12 | * @see Short#compareUnsigned 13 | * @see Short#toUnsignedInt 14 | */ 15 | @Documented 16 | @Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD}) 17 | public @interface Unsigned {} 18 | -------------------------------------------------------------------------------- /src/main/java/io/github/treesitter/jtreesitter/internal/ChainedLibraryLookup.java: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.jtreesitter.internal; 2 | 3 | import io.github.treesitter.jtreesitter.NativeLibraryLookup; 4 | import java.lang.foreign.Arena; 5 | import java.lang.foreign.Linker; 6 | import java.lang.foreign.SymbolLookup; 7 | import java.util.Optional; 8 | import java.util.ServiceLoader; 9 | 10 | @SuppressWarnings("unused") 11 | final class ChainedLibraryLookup implements NativeLibraryLookup { 12 | private ChainedLibraryLookup() {} 13 | 14 | static ChainedLibraryLookup INSTANCE = new ChainedLibraryLookup(); 15 | 16 | @Override 17 | public SymbolLookup get(Arena arena) { 18 | var serviceLoader = ServiceLoader.load(NativeLibraryLookup.class); 19 | // NOTE: can't use _ because of palantir/palantir-java-format#934 20 | SymbolLookup lookup = (name) -> Optional.empty(); 21 | for (var libraryLookup : serviceLoader) { 22 | lookup = lookup.or(libraryLookup.get(arena)); 23 | } 24 | return lookup.or(findLibrary(arena)).or(Linker.nativeLinker().defaultLookup()); 25 | } 26 | 27 | private static SymbolLookup findLibrary(Arena arena) { 28 | try { 29 | var library = System.mapLibraryName("tree-sitter"); 30 | return SymbolLookup.libraryLookup(library, arena); 31 | } catch (IllegalArgumentException e) { 32 | return SymbolLookup.loaderLookup(); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/io/github/treesitter/jtreesitter/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Java bindings to the tree-sitter parsing library. 3 | * 4 | *

Requirements

5 | * 6 | * 17 | * 18 | *

Basic Usage

19 | * 20 | * {@snippet lang = java: 21 | * Language language = new Language(TreeSitterJava.language()); 22 | * try (Parser parser = new Parser(language)) { 23 | * try (Tree tree = parser.parse("void main() {}", InputEncoding.UTF_8).orElseThrow()) { 24 | * Node rootNode = tree.getRootNode(); 25 | * assert rootNode.getType().equals("program"); 26 | * assert rootNode.getStartPoint().column() == 0; 27 | * assert rootNode.getEndPoint().column() == 14; 28 | * } 29 | * } 30 | *} 31 | * 32 | *

Library Loading

33 | * 34 | * There are three ways to load the shared libraries: 35 | * 36 | *
    37 | *
  1. 38 | * The libraries can be installed in the OS-specific library search path or in 39 | * {@systemProperty java.library.path}. The search path can be amended using the 40 | * {@code LD_LIBRARY_PATH} environment variable on Linux, {@code DYLD_LIBRARY_PATH} 41 | * on macOS, or {@code PATH} on Windows. The libraries will be loaded automatically by 42 | * {@link java.lang.foreign.SymbolLookup#libraryLookup(String, java.lang.foreign.Arena) 43 | * SymbolLookup.libraryLookup(String, Arena)}. 44 | *
  2. 45 | *
  3. 46 | * The libraries can be loaded manually by calling 47 | * {@link java.lang.System#loadLibrary(String) System.loadLibrary(String)}, 48 | * if the library is installed in {@systemProperty java.library.path}, 49 | * or {@link java.lang.System#load(String) System.load(String)}. 50 | *
  4. 51 | *
  5. 52 | * The libraries can be loaded manually by registering a custom implementation of 53 | * {@link io.github.treesitter.jtreesitter.NativeLibraryLookup NativeLibraryLookup}. 54 | * This can be used, for example, to load libraries from inside a JAR file. 55 | *
  6. 56 | *
57 | */ 58 | package io.github.treesitter.jtreesitter; 59 | -------------------------------------------------------------------------------- /src/test/java/io/github/treesitter/jtreesitter/LanguageTest.java: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.jtreesitter; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | 5 | import io.github.treesitter.jtreesitter.languages.TreeSitterJava; 6 | import org.junit.jupiter.api.BeforeAll; 7 | import org.junit.jupiter.api.Test; 8 | 9 | public class LanguageTest { 10 | private static Language language; 11 | 12 | @BeforeAll 13 | static void beforeAll() { 14 | language = new Language(TreeSitterJava.language()); 15 | } 16 | 17 | @Test 18 | void getAbiVersion() { 19 | assertEquals(14, language.getAbiVersion()); 20 | } 21 | 22 | @Test 23 | void getName() { 24 | assertNull(language.getName()); 25 | } 26 | 27 | @Test 28 | void getMetadata() { 29 | assertNull(language.getMetadata()); 30 | } 31 | 32 | @Test 33 | void getSymbolCount() { 34 | assertEquals(321, language.getSymbolCount()); 35 | } 36 | 37 | @Test 38 | void getStateCount() { 39 | assertEquals(1385, language.getStateCount()); 40 | } 41 | 42 | @Test 43 | void getFieldCount() { 44 | assertEquals(40, language.getFieldCount()); 45 | } 46 | 47 | @Test 48 | void getSymbolName() { 49 | assertEquals("identifier", language.getSymbolName((short) 1)); 50 | assertNull(language.getSymbolName((short) 999)); 51 | } 52 | 53 | @Test 54 | void getSymbolForName() { 55 | assertEquals((short) 138, language.getSymbolForName("program", true)); 56 | assertEquals((short) 0, language.getSymbolForName("$", false)); 57 | } 58 | 59 | @Test 60 | void getSupertypes() { 61 | assertArrayEquals(new short[0], language.getSupertypes()); 62 | } 63 | 64 | @Test 65 | void getSubtypes() { 66 | assertArrayEquals(new short[0], language.getSubtypes((short) 1)); 67 | } 68 | 69 | @Test 70 | void isNamed() { 71 | assertTrue(language.isNamed((short) 1)); 72 | } 73 | 74 | @Test 75 | void isVisible() { 76 | assertTrue(language.isVisible((short) 1)); 77 | } 78 | 79 | @Test 80 | void isSupertype() { 81 | assertFalse(language.isSupertype((short) 1)); 82 | } 83 | 84 | @Test 85 | void getFieldNameForId() { 86 | assertNotNull(language.getFieldNameForId((short) 20)); 87 | } 88 | 89 | @Test 90 | void getFieldIdForName() { 91 | assertEquals(20, language.getFieldIdForName("name")); 92 | } 93 | 94 | @Test 95 | void nextState() { 96 | assertNotEquals(0, language.nextState((short) 1, (short) 138)); 97 | } 98 | 99 | @Test 100 | void lookaheadIterator() { 101 | assertDoesNotThrow(() -> { 102 | var state = language.nextState((short) 1, (short) 138); 103 | language.lookaheadIterator(state).close(); 104 | }); 105 | } 106 | 107 | @Test 108 | void testEquals() { 109 | var other = new Language(TreeSitterJava.language()); 110 | assertEquals(other, language.clone()); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/test/java/io/github/treesitter/jtreesitter/LookaheadIteratorTest.java: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.jtreesitter; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | 5 | import io.github.treesitter.jtreesitter.languages.TreeSitterJava; 6 | import java.util.List; 7 | import org.junit.jupiter.api.*; 8 | 9 | class LookaheadIteratorTest { 10 | private static Language language; 11 | private static short state; 12 | private LookaheadIterator lookahead; 13 | 14 | @BeforeAll 15 | static void beforeAll() { 16 | language = new Language(TreeSitterJava.language()); 17 | state = language.nextState((short) 1, (short) 138); 18 | } 19 | 20 | @BeforeEach 21 | void setUp() { 22 | lookahead = language.lookaheadIterator(state); 23 | } 24 | 25 | @AfterEach 26 | void tearDown() { 27 | lookahead.close(); 28 | } 29 | 30 | @Test 31 | void getLanguage() { 32 | assertEquals(language, lookahead.getLanguage()); 33 | } 34 | 35 | @Test 36 | void getCurrentSymbol() { 37 | assertEquals((short) -1, lookahead.getCurrentSymbol()); 38 | } 39 | 40 | @Test 41 | void getCurrentSymbolName() { 42 | assertEquals("ERROR", lookahead.getCurrentSymbolName()); 43 | } 44 | 45 | @Test 46 | @DisplayName("reset(state)") 47 | void resetState() { 48 | assertDoesNotThrow(() -> lookahead.next()); 49 | assertTrue(lookahead.reset(state)); 50 | assertEquals("ERROR", lookahead.getCurrentSymbolName()); 51 | } 52 | 53 | @Test 54 | @DisplayName("reset(language)") 55 | void resetLanguage() { 56 | assertDoesNotThrow(() -> lookahead.next()); 57 | assertTrue(lookahead.reset(state, language)); 58 | assertEquals("ERROR", lookahead.getCurrentSymbolName()); 59 | } 60 | 61 | @Test 62 | void hasNext() { 63 | assertTrue(lookahead.hasNext()); 64 | assertEquals("ERROR", lookahead.getCurrentSymbolName()); 65 | } 66 | 67 | @Test 68 | void next() { 69 | assertEquals("end", lookahead.next().name()); 70 | } 71 | 72 | @Test 73 | void symbols() { 74 | assertEquals(3, lookahead.symbols().count()); 75 | } 76 | 77 | @Test 78 | void names() { 79 | var names = List.of("end", "line_comment", "block_comment"); 80 | assertEquals(names, lookahead.names().toList()); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/test/java/io/github/treesitter/jtreesitter/NodeTest.java: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.jtreesitter; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | 5 | import io.github.treesitter.jtreesitter.languages.TreeSitterJava; 6 | import org.junit.jupiter.api.*; 7 | 8 | class NodeTest { 9 | private static Tree tree; 10 | private static Node node; 11 | 12 | @BeforeAll 13 | static void beforeAll() { 14 | var language = new Language(TreeSitterJava.language()); 15 | try (var parser = new Parser(language)) { 16 | tree = parser.parse("class Foo {} // uni©ode").orElseThrow(); 17 | node = tree.getRootNode(); 18 | } 19 | } 20 | 21 | @AfterAll 22 | static void afterAll() { 23 | tree.close(); 24 | } 25 | 26 | @Test 27 | void getTree() { 28 | assertSame(tree, node.getTree()); 29 | } 30 | 31 | @Test 32 | void getId() { 33 | assertNotEquals(0L, node.getId()); 34 | } 35 | 36 | @Test 37 | void getSymbol() { 38 | assertEquals(138, node.getSymbol()); 39 | } 40 | 41 | @Test 42 | void getGrammarSymbol() { 43 | assertEquals(138, node.getGrammarSymbol()); 44 | } 45 | 46 | @Test 47 | void getType() { 48 | assertEquals("program", node.getType()); 49 | } 50 | 51 | @Test 52 | void getGrammarType() { 53 | assertEquals("program", node.getGrammarType()); 54 | } 55 | 56 | @Test 57 | void isNamed() { 58 | assertTrue(node.isNamed()); 59 | } 60 | 61 | @Test 62 | void isExtra() { 63 | assertFalse(node.isExtra()); 64 | } 65 | 66 | @Test 67 | void isError() { 68 | assertFalse(node.isError()); 69 | } 70 | 71 | @Test 72 | void isMissing() { 73 | assertFalse(node.isMissing()); 74 | } 75 | 76 | @Test 77 | void hasChanges() { 78 | assertFalse(node.hasChanges()); 79 | } 80 | 81 | @Test 82 | void hasError() { 83 | assertFalse(node.hasError()); 84 | } 85 | 86 | @Test 87 | void getParseState() { 88 | assertEquals(0, node.getParseState()); 89 | } 90 | 91 | @Test 92 | void getNextParseState() { 93 | assertEquals(0, node.getNextParseState()); 94 | } 95 | 96 | @Test 97 | void getStartByte() { 98 | assertEquals(0, node.getStartByte()); 99 | } 100 | 101 | @Test 102 | void getEndByte() { 103 | assertEquals(24, node.getEndByte()); 104 | } 105 | 106 | @Test 107 | void getRange() { 108 | Point startPoint = new Point(0, 0), endPoint = new Point(0, 24); 109 | assertEquals(new Range(startPoint, endPoint, 0, 24), node.getRange()); 110 | } 111 | 112 | @Test 113 | void getStartPoint() { 114 | assertEquals(new Point(0, 0), node.getStartPoint()); 115 | } 116 | 117 | @Test 118 | void getEndPoint() { 119 | assertEquals(new Point(0, 24), node.getEndPoint()); 120 | } 121 | 122 | @Test 123 | void getChildCount() { 124 | assertEquals(2, node.getChildCount()); 125 | } 126 | 127 | @Test 128 | void getNamedChildCount() { 129 | assertEquals(2, node.getNamedChildCount()); 130 | } 131 | 132 | @Test 133 | void getDescendantCount() { 134 | assertEquals(8, node.getDescendantCount()); 135 | } 136 | 137 | @Test 138 | void getParent() { 139 | assertTrue(node.getParent().isEmpty()); 140 | } 141 | 142 | @Test 143 | void getNextSibling() { 144 | assertTrue(node.getNextSibling().isEmpty()); 145 | } 146 | 147 | @Test 148 | void getPrevSibling() { 149 | assertTrue(node.getPrevSibling().isEmpty()); 150 | } 151 | 152 | @Test 153 | void getNextNamedSibling() { 154 | assertTrue(node.getNextNamedSibling().isEmpty()); 155 | } 156 | 157 | @Test 158 | void getPrevNamedSibling() { 159 | assertTrue(node.getPrevNamedSibling().isEmpty()); 160 | } 161 | 162 | @Test 163 | void getChild() { 164 | var child = node.getChild(0).orElseThrow(); 165 | assertEquals("class_declaration", child.getType()); 166 | } 167 | 168 | @Test 169 | void getNamedChild() { 170 | var child = node.getNamedChild(0).orElseThrow(); 171 | assertEquals("class_declaration", child.getGrammarType()); 172 | } 173 | 174 | @Test 175 | void getFirstChildForByte() { 176 | var child = node.getFirstChildForByte(15).orElseThrow(); 177 | assertEquals("line_comment", child.getGrammarType()); 178 | } 179 | 180 | @Test 181 | void getFirstNamedChildForByte() { 182 | var child = node.getFirstNamedChildForByte(15).orElseThrow(); 183 | assertEquals("line_comment", child.getGrammarType()); 184 | } 185 | 186 | @Test 187 | void getChildByFieldId() { 188 | var child = node.getChild(0).orElseThrow(); 189 | child = child.getChildByFieldId((short) 20).orElseThrow(); 190 | assertEquals("identifier", child.getType()); 191 | } 192 | 193 | @Test 194 | void getChildByFieldName() { 195 | var child = node.getChild(0).orElseThrow(); 196 | child = child.getChildByFieldName("name").orElseThrow(); 197 | assertEquals("identifier", child.getGrammarType()); 198 | } 199 | 200 | @Test 201 | void getChildren() { 202 | var children = node.getChild(0).orElseThrow().getChildren(); 203 | assertEquals(3, children.size()); 204 | assertEquals("class", children.getFirst().getType()); 205 | } 206 | 207 | @Test 208 | void getNamedChildren() { 209 | var children = node.getChild(0).orElseThrow().getNamedChildren(); 210 | assertEquals(2, children.size()); 211 | assertEquals("identifier", children.getFirst().getType()); 212 | } 213 | 214 | @Test 215 | void getChildrenByFieldId() { 216 | var children = node.getChild(0).orElseThrow().getChildrenByFieldId((short) 1); 217 | assertTrue(children.isEmpty()); 218 | } 219 | 220 | @Test 221 | void getChildrenByFieldName() { 222 | var children = node.getChild(0).orElseThrow().getChildrenByFieldName("body"); 223 | assertEquals(1, children.size()); 224 | assertEquals("class_body", children.getFirst().getType()); 225 | } 226 | 227 | @Test 228 | void getFieldNameForChild() { 229 | var child = node.getChild(0).orElseThrow(); 230 | assertNull(child.getFieldNameForChild(0)); 231 | assertEquals("body", child.getFieldNameForChild(2)); 232 | } 233 | 234 | @Test 235 | void getFieldNameForNamedChild() { 236 | var child = node.getChild(0).orElseThrow(); 237 | assertNull(child.getFieldNameForNamedChild(2)); 238 | } 239 | 240 | @Test 241 | @DisplayName("getDescendant(bytes)") 242 | void getDescendantBytes() { 243 | var descendant = node.getDescendant(0, 5).orElseThrow(); 244 | assertEquals("class", descendant.getType()); 245 | } 246 | 247 | @Test 248 | @DisplayName("getDescendant(points)") 249 | void getDescendantPoints() { 250 | Point startPoint = new Point(0, 10), endPoint = new Point(0, 12); 251 | var descendant = node.getDescendant(startPoint, endPoint).orElseThrow(); 252 | assertEquals("class_body", descendant.getGrammarType()); 253 | } 254 | 255 | @Test 256 | @DisplayName("getNamedDescendant(bytes)") 257 | void getNamedDescendantBytes() { 258 | var descendant = node.getNamedDescendant(0, 5).orElseThrow(); 259 | assertEquals("class_declaration", descendant.getType()); 260 | } 261 | 262 | @Test 263 | @DisplayName("getNamedDescendant(points)") 264 | void getNamedDescendantPoints() { 265 | Point startPoint = new Point(0, 6), endPoint = new Point(0, 9); 266 | var descendant = node.getNamedDescendant(startPoint, endPoint).orElseThrow(); 267 | assertEquals("identifier", descendant.getGrammarType()); 268 | } 269 | 270 | @Test 271 | void getChildWithDescendant() { 272 | var descendant = node.getChild(0).orElseThrow(); 273 | var child = node.getChildWithDescendant(descendant); 274 | assertEquals("class_declaration", child.orElseThrow().getType()); 275 | } 276 | 277 | @Test 278 | void getText() { 279 | var child = node.getChild(1).orElseThrow(); 280 | assertEquals("// uni©ode", child.getText()); 281 | } 282 | 283 | @Test 284 | void edit() { 285 | var edit = new InputEdit(0, 12, 10, new Point(0, 0), new Point(0, 12), new Point(0, 10)); 286 | try (var copy = tree.clone()) { 287 | var node = copy.getRootNode(); 288 | copy.edit(edit); 289 | node.edit(edit); 290 | assertTrue(node.hasChanges()); 291 | } 292 | } 293 | 294 | @Test 295 | void walk() { 296 | var child = node.getChild(0).orElseThrow(); 297 | try (var cursor = child.walk()) { 298 | assertEquals(child, cursor.getCurrentNode()); 299 | } 300 | } 301 | 302 | @Test 303 | void toSexp() { 304 | var sexp = "(program (class_declaration name: (identifier) body: (class_body)) (line_comment))"; 305 | assertEquals(sexp, node.toSexp()); 306 | } 307 | 308 | @Test 309 | void equals() { 310 | var other = node.getChild(0).orElseThrow(); 311 | assertNotEquals(node, other); 312 | other = other.getParent().orElseThrow(); 313 | assertEquals(node, other); 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /src/test/java/io/github/treesitter/jtreesitter/ParserTest.java: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.jtreesitter; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | 5 | import io.github.treesitter.jtreesitter.languages.TreeSitterJava; 6 | import java.nio.charset.StandardCharsets; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.concurrent.*; 10 | import org.junit.jupiter.api.*; 11 | 12 | class ParserTest { 13 | private static Language language; 14 | private Parser parser; 15 | 16 | @BeforeAll 17 | static void beforeAll() { 18 | language = new Language(TreeSitterJava.language()); 19 | } 20 | 21 | @BeforeEach 22 | void setUp() { 23 | parser = new Parser(); 24 | } 25 | 26 | @AfterEach 27 | void tearDown() { 28 | parser.close(); 29 | } 30 | 31 | @Test 32 | void getLanguage() { 33 | assertNull(parser.getLanguage()); 34 | } 35 | 36 | @Test 37 | void setLanguage() { 38 | assertSame(parser, parser.setLanguage(language)); 39 | assertEquals(language, parser.getLanguage()); 40 | } 41 | 42 | @Test 43 | void setLogger() { 44 | assertSame(parser, parser.setLogger(null)); 45 | } 46 | 47 | @Test 48 | void getIncludedRanges() { 49 | assertEquals(1, parser.getIncludedRanges().size()); 50 | } 51 | 52 | @Test 53 | void setIncludedRanges() { 54 | var range = new Range(Point.MIN, new Point(0, 1), 0, 1); 55 | assertSame(parser, parser.setIncludedRanges(List.of(range))); 56 | assertIterableEquals(List.of(range), parser.getIncludedRanges()); 57 | assertThrows(IllegalArgumentException.class, () -> parser.setIncludedRanges(List.of(range, range))); 58 | } 59 | 60 | @Test 61 | @DisplayName("parse(utf8)") 62 | void parseUtf8() { 63 | parser.setLanguage(language); 64 | try (var tree = parser.parse("class Foo {}").orElseThrow()) { 65 | var rootNode = tree.getRootNode(); 66 | 67 | assertEquals(12, rootNode.getEndByte()); 68 | assertFalse(rootNode.isError()); 69 | assertEquals("(program (class_declaration name: (identifier) body: (class_body)))", rootNode.toSexp()); 70 | } 71 | } 72 | 73 | @Test 74 | @DisplayName("parse(utf16)") 75 | void parseUtf16() { 76 | parser.setLanguage(language); 77 | var encoding = InputEncoding.valueOf(StandardCharsets.UTF_16); 78 | try (var tree = parser.parse("var java = \"💩\";", encoding).orElseThrow()) { 79 | var rootNode = tree.getRootNode(); 80 | 81 | assertEquals(32, rootNode.getEndByte()); 82 | assertFalse(rootNode.isError()); 83 | assertEquals( 84 | "(program (local_variable_declaration type: (type_identifier) declarator: (variable_declarator name: (identifier) value: (string_literal (string_fragment)))))", 85 | rootNode.toSexp()); 86 | } 87 | } 88 | 89 | @Test 90 | @DisplayName("parse(logger)") 91 | void parseLogger() { 92 | var messages = new ArrayList(); 93 | parser.setLanguage(language) 94 | .setLogger((type, message) -> messages.add("%s - %s".formatted(type.name(), message))) 95 | .parse("class Foo {}") 96 | .orElseThrow() 97 | .close(); 98 | assertEquals(44, messages.size()); 99 | assertEquals("LEX - new_parse", messages.getFirst()); 100 | assertEquals("PARSE - consume character:'c'", messages.get(3)); 101 | assertEquals("LEX - done", messages.getLast()); 102 | } 103 | 104 | @SuppressWarnings("unused") 105 | @Test 106 | @DisplayName("parse(callback)") 107 | void parseCallback() { 108 | var source = "class Foo {}"; 109 | // NOTE: can't use _ because of palantir/palantir-java-format#934 110 | ParseCallback callback = (offset, p) -> source.substring(offset, Integer.min(offset, source.length())); 111 | parser.setLanguage(language); 112 | try (var tree = parser.parse(callback, InputEncoding.UTF_8).orElseThrow()) { 113 | assertNull(tree.getText()); 114 | assertEquals("program", tree.getRootNode().getType()); 115 | } 116 | } 117 | 118 | @Test 119 | @DisplayName("parse(timeout)") 120 | @SuppressWarnings("deprecation") 121 | void parseTimeout() { 122 | var source = "}".repeat(1024); 123 | // NOTE: can't use _ because of palantir/palantir-java-format#934 124 | ParseCallback callback = (offset, p) -> source.substring(offset, Integer.min(source.length(), offset + 1)); 125 | 126 | parser.setLanguage(language).setTimeoutMicros(2L); 127 | assertTrue(parser.parse(callback, InputEncoding.UTF_8).isEmpty()); 128 | } 129 | 130 | @Test 131 | @DisplayName("parse(cancellation)") 132 | @SuppressWarnings("deprecation") 133 | void parseCancellation() { 134 | var source = "}".repeat(1024 * 1024); 135 | // NOTE: can't use _ because of palantir/palantir-java-format#934 136 | ParseCallback callback = (offset, p) -> source.substring(offset, Integer.min(source.length(), offset + 1)); 137 | 138 | var flag = new Parser.CancellationFlag(); 139 | parser.setLanguage(language).setCancellationFlag(flag); 140 | try (var service = Executors.newFixedThreadPool(2)) { 141 | service.submit(() -> { 142 | try { 143 | wait(10L); 144 | } catch (InterruptedException e) { 145 | service.shutdownNow(); 146 | } finally { 147 | flag.set(1L); 148 | } 149 | }); 150 | var result = service.submit(() -> parser.parse(callback, InputEncoding.UTF_8)); 151 | assertTrue(result.get(30L, TimeUnit.MILLISECONDS).isEmpty()); 152 | } catch (InterruptedException | CancellationException | ExecutionException | TimeoutException e) { 153 | fail("Parsing was not halted gracefully", e); 154 | } 155 | } 156 | 157 | @Test 158 | @DisplayName("parse(options)") 159 | void parseOptions() { 160 | var source = "}".repeat(1024); 161 | // NOTE: can't use _ because of palantir/palantir-java-format#934 162 | ParseCallback callback = (offset, p) -> source.substring(offset, Integer.min(source.length(), offset + 1)); 163 | var options = new Parser.Options((state) -> state.getCurrentByteOffset() >= 1000); 164 | 165 | parser.setLanguage(language); 166 | assertTrue(parser.parse(callback, InputEncoding.UTF_8, options).isEmpty()); 167 | } 168 | 169 | @Test 170 | void reset() { 171 | var source = "class foo bar() {}"; 172 | // NOTE: can't use _ because of palantir/palantir-java-format#934 173 | ParseCallback callback = (offset, p) -> source.substring(offset, Integer.min(source.length(), offset + 1)); 174 | var options = new Parser.Options(Parser.State::hasError); 175 | 176 | parser.setLanguage(language); 177 | parser.parse(callback, InputEncoding.UTF_8, options); 178 | parser.reset(); 179 | try (var tree = parser.parse("String foo;").orElseThrow()) { 180 | assertFalse(tree.getRootNode().hasError()); 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/test/java/io/github/treesitter/jtreesitter/QueryCursorTest.java: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.jtreesitter; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | 5 | import io.github.treesitter.jtreesitter.languages.TreeSitterJava; 6 | import java.util.function.Consumer; 7 | import org.junit.jupiter.api.AfterAll; 8 | import org.junit.jupiter.api.BeforeAll; 9 | import org.junit.jupiter.api.Test; 10 | 11 | public class QueryCursorTest { 12 | private static Language language; 13 | private static Parser parser; 14 | private static final String source = 15 | """ 16 | (identifier) @identifier 17 | 18 | (class_declaration 19 | name: (identifier) @class 20 | (class_body) @body) 21 | """ 22 | .stripIndent(); 23 | 24 | @BeforeAll 25 | static void beforeAll() { 26 | language = new Language(TreeSitterJava.language()); 27 | parser = new Parser(language); 28 | } 29 | 30 | @AfterAll 31 | static void afterAll() { 32 | parser.close(); 33 | } 34 | 35 | private static void assertCursor(Consumer assertions) { 36 | assertCursor(source, assertions); 37 | } 38 | 39 | private static void assertCursor(String source, Consumer assertions) { 40 | try (var query = new Query(language, source)) { 41 | try (var cursor = new QueryCursor(query)) { 42 | assertions.accept(cursor); 43 | } 44 | } 45 | } 46 | 47 | @Test 48 | void getMatchLimit() { 49 | assertCursor(cursor -> assertEquals(-1, cursor.getMatchLimit())); 50 | } 51 | 52 | @Test 53 | void setMatchLimit() { 54 | assertCursor(cursor -> { 55 | assertSame(cursor, cursor.setMatchLimit(10)); 56 | assertEquals(10, cursor.getMatchLimit()); 57 | }); 58 | } 59 | 60 | @Test 61 | void setMaxStartDepth() { 62 | assertCursor(cursor -> assertSame(cursor, cursor.setMaxStartDepth(10))); 63 | } 64 | 65 | @Test 66 | void setByteRange() { 67 | assertCursor(cursor -> assertSame(cursor, cursor.setByteRange(1, 10))); 68 | } 69 | 70 | @Test 71 | void setPointRange() { 72 | assertCursor(cursor -> { 73 | Point start = new Point(0, 1), end = new Point(1, 10); 74 | assertSame(cursor, cursor.setPointRange(start, end)); 75 | }); 76 | } 77 | 78 | @Test 79 | void didExceedMatchLimit() { 80 | assertCursor(cursor -> assertFalse(cursor.didExceedMatchLimit())); 81 | } 82 | 83 | @Test 84 | void findCaptures() { 85 | try (var tree = parser.parse("class Foo {}").orElseThrow()) { 86 | assertCursor(cursor -> { 87 | var matches = cursor.findCaptures(tree.getRootNode()).toList(); 88 | assertEquals(3, matches.size()); 89 | assertEquals(0, matches.get(0).getKey()); 90 | assertEquals(0, matches.get(1).getKey()); 91 | assertNotEquals(matches.get(0).getValue(), matches.get(1).getValue()); 92 | }); 93 | } 94 | } 95 | 96 | @Test 97 | void findMatches() { 98 | try (var tree = parser.parse("class Foo {}").orElseThrow()) { 99 | assertCursor(cursor -> { 100 | var matches = cursor.findMatches(tree.getRootNode()).toList(); 101 | assertEquals(2, matches.size()); 102 | assertEquals(0, matches.getFirst().patternIndex()); 103 | assertEquals(1, matches.getLast().patternIndex()); 104 | }); 105 | } 106 | 107 | try (var tree = parser.parse("int y = x + 1;").orElseThrow()) { 108 | var source = 109 | """ 110 | ((variable_declarator 111 | (identifier) @y 112 | (binary_expression 113 | (identifier) @x)) 114 | (#not-eq? @y @x)) 115 | """ 116 | .stripIndent(); 117 | assertCursor(source, cursor -> { 118 | var matches = cursor.findMatches(tree.getRootNode()).toList(); 119 | assertEquals(1, matches.size()); 120 | assertEquals( 121 | "y", matches.getFirst().captures().getFirst().node().getText()); 122 | }); 123 | } 124 | 125 | try (var tree = parser.parse("class Foo{}\nclass Bar {}").orElseThrow()) { 126 | var source = """ 127 | ((identifier) @foo 128 | (#eq? @foo "Foo")) 129 | """ 130 | .stripIndent(); 131 | assertCursor(source, cursor -> { 132 | var matches = cursor.findMatches(tree.getRootNode()).toList(); 133 | assertEquals(1, matches.size()); 134 | assertEquals( 135 | "Foo", matches.getFirst().captures().getFirst().node().getText()); 136 | }); 137 | 138 | source = """ 139 | ((identifier) @name 140 | (#not-any-of? @name "Foo" "Bar")) 141 | """ 142 | .stripIndent(); 143 | assertCursor(source, cursor -> { 144 | var matches = cursor.findMatches(tree.getRootNode()).toList(); 145 | assertTrue(matches.isEmpty()); 146 | }); 147 | 148 | source = """ 149 | ((identifier) @foo 150 | (#ieq? @foo "foo")) 151 | """ 152 | .stripIndent(); 153 | assertCursor(source, cursor -> { 154 | var options = new QueryCursor.Options((predicate, match) -> { 155 | if (!predicate.getName().equals("ieq?")) return true; 156 | var args = predicate.getArgs(); 157 | var node = match.findNodes(args.getFirst().value()).getFirst(); 158 | return args.getLast().value().equalsIgnoreCase(node.getText()); 159 | }); 160 | var matches = cursor.findMatches(tree.getRootNode(), options).toList(); 161 | assertEquals(1, matches.size()); 162 | assertEquals( 163 | "Foo", matches.getFirst().captures().getFirst().node().getText()); 164 | }); 165 | } 166 | 167 | // Verify that capture count is treated as `uint16_t` and not as signed Java `short` 168 | try (var tree = parser.parse(";".repeat(Short.MAX_VALUE + 1)).orElseThrow()) { 169 | var source = """ 170 | ";"+ @capture 171 | """; 172 | assertCursor(source, cursor -> { 173 | var matches = cursor.findMatches(tree.getRootNode()).toList(); 174 | assertEquals(1, matches.size()); 175 | assertEquals(Short.MAX_VALUE + 1, matches.getFirst().captures().size()); 176 | }); 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/test/java/io/github/treesitter/jtreesitter/QueryTest.java: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.jtreesitter; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | 5 | import io.github.treesitter.jtreesitter.languages.TreeSitterJava; 6 | import java.util.List; 7 | import java.util.NoSuchElementException; 8 | import java.util.function.Consumer; 9 | import org.junit.jupiter.api.AfterAll; 10 | import org.junit.jupiter.api.BeforeAll; 11 | import org.junit.jupiter.api.Test; 12 | 13 | class QueryTest { 14 | private static Language language; 15 | private static Parser parser; 16 | private static final String source = 17 | """ 18 | (identifier) @identifier 19 | 20 | (class_declaration 21 | name: (identifier) @class 22 | (class_body) @body) 23 | """ 24 | .stripIndent(); 25 | 26 | @BeforeAll 27 | static void beforeAll() { 28 | language = new Language(TreeSitterJava.language()); 29 | parser = new Parser(language); 30 | } 31 | 32 | @AfterAll 33 | static void afterAll() { 34 | parser.close(); 35 | } 36 | 37 | @SuppressWarnings("unused") 38 | private static void assertError(Class type, String source, String message) { 39 | // NOTE: can't use _ because of palantir/palantir-java-format#934 40 | try (var q = new Query(language, source)) { 41 | fail("Expected QueryError to be thrown, but nothing was thrown."); 42 | } catch (QueryError ex) { 43 | assertInstanceOf(type, ex); 44 | assertEquals(message, ex.getMessage()); 45 | } 46 | } 47 | 48 | private static void assertQuery(Consumer assertions) { 49 | assertQuery(source, assertions); 50 | } 51 | 52 | private static void assertQuery(String source, Consumer assertions) { 53 | try (var query = new Query(language, source)) { 54 | assertions.accept(query); 55 | } catch (QueryError e) { 56 | fail("Unexpected query error", e); 57 | } 58 | } 59 | 60 | @Test 61 | void errors() { 62 | assertError(QueryError.Syntax.class, "(identifier) @", "Unexpected EOF"); 63 | assertError(QueryError.Syntax.class, " identifier)", "Invalid syntax at row 0, column 1"); 64 | 65 | assertError( 66 | QueryError.Capture.class, 67 | "((identifier) @foo\n (#test? @bar))", 68 | "Invalid capture name at row 1, column 10: bar"); 69 | 70 | assertError(QueryError.NodeType.class, "(foo)", "Invalid node type at row 0, column 1: foo"); 71 | 72 | assertError(QueryError.Field.class, "foo: (identifier)", "Invalid field name at row 0, column 0: foo"); 73 | 74 | assertError(QueryError.Structure.class, "(program (identifier))", "Impossible pattern at row 0, column 9"); 75 | 76 | assertError( 77 | QueryError.Predicate.class, 78 | "\n((identifier) @foo\n (#any-of?))", 79 | "Invalid predicate in pattern at row 1: #any-of? expects at least 2 arguments, got 0"); 80 | } 81 | 82 | @Test 83 | void getPatternCount() { 84 | assertQuery(query -> assertEquals(2, query.getPatternCount())); 85 | } 86 | 87 | @Test 88 | void getCaptureNames() { 89 | assertQuery(query -> assertIterableEquals(List.of("identifier", "class", "body"), query.getCaptureNames())); 90 | } 91 | 92 | @Test 93 | void getStringValues() { 94 | var source = """ 95 | ((identifier) @foo 96 | (#eq? @foo "Foo")) 97 | """ 98 | .stripIndent(); 99 | assertQuery(source, query -> assertIterableEquals(List.of("eq?", "Foo"), query.getStringValues())); 100 | } 101 | 102 | @Test 103 | void disablePattern() { 104 | assertQuery(query -> { 105 | assertDoesNotThrow(() -> query.disablePattern(1)); 106 | assertThrows(IndexOutOfBoundsException.class, () -> query.disablePattern(2)); 107 | }); 108 | } 109 | 110 | @Test 111 | void disableCapture() { 112 | assertQuery(query -> { 113 | assertDoesNotThrow(() -> query.disableCapture("body")); 114 | assertThrows(NoSuchElementException.class, () -> query.disableCapture("none")); 115 | }); 116 | } 117 | 118 | @Test 119 | void startByteForPattern() { 120 | assertQuery(query -> { 121 | assertEquals(26, query.startByteForPattern(1)); 122 | assertThrows(IndexOutOfBoundsException.class, () -> query.startByteForPattern(2)); 123 | }); 124 | } 125 | 126 | @Test 127 | void endByteForPattern() { 128 | assertQuery(query -> { 129 | assertEquals(26, query.endByteForPattern(0)); 130 | assertThrows(IndexOutOfBoundsException.class, () -> query.endByteForPattern(2)); 131 | }); 132 | } 133 | 134 | @Test 135 | void isPatternRooted() { 136 | assertQuery(query -> { 137 | assertTrue(query.isPatternRooted(0)); 138 | assertThrows(IndexOutOfBoundsException.class, () -> query.isPatternRooted(2)); 139 | }); 140 | } 141 | 142 | @Test 143 | void isPatternNonLocal() { 144 | assertQuery(query -> { 145 | assertFalse(query.isPatternNonLocal(0)); 146 | assertThrows(IndexOutOfBoundsException.class, () -> query.isPatternNonLocal(2)); 147 | }); 148 | } 149 | 150 | @Test 151 | void isPatternGuaranteedAtStep() { 152 | assertQuery(query -> { 153 | assertFalse(query.isPatternGuaranteedAtStep(27)); 154 | assertThrows(IndexOutOfBoundsException.class, () -> query.isPatternGuaranteedAtStep(99)); 155 | }); 156 | } 157 | 158 | @Test 159 | void getPatternSettings() { 160 | assertQuery("((identifier) @foo (#set! foo))", query -> { 161 | var settings = query.getPatternSettings(0); 162 | assertTrue(settings.get("foo").isEmpty()); 163 | }); 164 | assertQuery("((identifier) @foo (#set! foo \"FOO\"))", query -> { 165 | var settings = query.getPatternSettings(0); 166 | assertEquals("FOO", settings.get("foo").orElse(null)); 167 | }); 168 | } 169 | 170 | @Test 171 | void getPatternAssertions() { 172 | assertQuery("((identifier) @foo (#is? foo))", query -> { 173 | var assertions = query.getPatternAssertions(0, true); 174 | assertTrue(assertions.get("foo").isEmpty()); 175 | }); 176 | assertQuery("((identifier) @foo (#is-not? foo \"FOO\"))", query -> { 177 | var assertions = query.getPatternAssertions(0, false); 178 | assertEquals("FOO", assertions.get("foo").orElse(null)); 179 | }); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/test/java/io/github/treesitter/jtreesitter/TreeCursorTest.java: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.jtreesitter; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | 5 | import io.github.treesitter.jtreesitter.languages.TreeSitterJava; 6 | import java.lang.foreign.Arena; 7 | import org.junit.jupiter.api.AfterAll; 8 | import org.junit.jupiter.api.AfterEach; 9 | import org.junit.jupiter.api.BeforeAll; 10 | import org.junit.jupiter.api.BeforeEach; 11 | import org.junit.jupiter.api.DisplayName; 12 | import org.junit.jupiter.api.Test; 13 | 14 | class TreeCursorTest { 15 | private static Tree tree; 16 | private TreeCursor cursor; 17 | 18 | @BeforeAll 19 | static void beforeAll() { 20 | var language = new Language(TreeSitterJava.language()); 21 | try (var parser = new Parser(language)) { 22 | tree = parser.parse("class Foo {}").orElseThrow(); 23 | } 24 | } 25 | 26 | @AfterAll 27 | static void afterAll() { 28 | tree.close(); 29 | } 30 | 31 | @BeforeEach 32 | void setUp() { 33 | cursor = tree.walk(); 34 | } 35 | 36 | @AfterEach 37 | void tearDown() { 38 | cursor.close(); 39 | } 40 | 41 | @Test 42 | void getCurrentNode() { 43 | var node = cursor.getCurrentNode(); 44 | assertEquals(tree.getRootNode(), node); 45 | assertSame(node, cursor.getCurrentNode()); 46 | 47 | try (var arena = Arena.ofConfined()) { 48 | try (var copy = cursor.clone()) { 49 | node = copy.getCurrentNode(arena); 50 | assertEquals(node, tree.getRootNode()); 51 | } 52 | // can still access node after cursor was closed 53 | assertEquals(node, tree.getRootNode()); 54 | } 55 | } 56 | 57 | @Test 58 | void getCurrentDepth() { 59 | assertEquals(0, cursor.getCurrentDepth()); 60 | } 61 | 62 | @Test 63 | void getCurrentFieldId() { 64 | assertEquals(0, cursor.getCurrentFieldId()); 65 | } 66 | 67 | @Test 68 | void getCurrentFieldName() { 69 | assertNull(cursor.getCurrentFieldName()); 70 | } 71 | 72 | @Test 73 | void getCurrentDescendantIndex() { 74 | assertEquals(0, cursor.getCurrentDescendantIndex()); 75 | } 76 | 77 | @Test 78 | void gotoFirstChild() { 79 | assertTrue(cursor.gotoFirstChild()); 80 | assertEquals(1, cursor.getCurrentDepth()); 81 | } 82 | 83 | @Test 84 | void gotoLastChild() { 85 | assertTrue(cursor.gotoLastChild()); 86 | assertEquals(1, cursor.getCurrentDescendantIndex()); 87 | } 88 | 89 | @Test 90 | void gotoParent() { 91 | assertFalse(cursor.gotoParent()); 92 | } 93 | 94 | @Test 95 | void gotoNextSibling() { 96 | assertTrue(cursor.gotoFirstChild()); 97 | assertFalse(cursor.gotoNextSibling()); 98 | } 99 | 100 | @Test 101 | void gotoPreviousSibling() { 102 | assertTrue(cursor.gotoLastChild()); 103 | assertFalse(cursor.gotoPreviousSibling()); 104 | } 105 | 106 | @Test 107 | void gotoDescendant() { 108 | cursor.gotoDescendant(2); 109 | assertEquals(2, cursor.getCurrentDescendantIndex()); 110 | assertEquals("class", cursor.getCurrentNode().getText()); 111 | } 112 | 113 | @Test 114 | void gotoFirstChildForByte() { 115 | assertEquals(0, cursor.gotoFirstChildForByte(1).orElseThrow()); 116 | assertEquals("class_declaration", cursor.getCurrentNode().getType()); 117 | assertTrue(cursor.gotoFirstChildForByte(13).isEmpty()); 118 | } 119 | 120 | @Test 121 | void gotoFirstChildForPoint() { 122 | assertTrue(cursor.gotoFirstChild()); 123 | assertEquals(1, cursor.gotoFirstChildForPoint(new Point(0, 7)).orElseThrow()); 124 | assertEquals("name", cursor.getCurrentFieldName()); 125 | assertTrue(cursor.gotoFirstChildForPoint(new Point(1, 0)).isEmpty()); 126 | } 127 | 128 | @Test 129 | @DisplayName("reset(node)") 130 | void resetNode() { 131 | var root = tree.getRootNode(); 132 | assertTrue(cursor.gotoFirstChild()); 133 | assertNotEquals(root, cursor.getCurrentNode()); 134 | cursor.reset(root); 135 | assertEquals(root, cursor.getCurrentNode()); 136 | } 137 | 138 | @Test 139 | @DisplayName("reset(cursor)") 140 | void resetCursor() { 141 | var copy = cursor.clone(); 142 | var root = tree.getRootNode(); 143 | assertTrue(cursor.gotoFirstChild()); 144 | assertNotEquals(root, cursor.getCurrentNode()); 145 | cursor.reset(copy); 146 | assertEquals(root, cursor.getCurrentNode()); 147 | } 148 | 149 | @Test 150 | @DisplayName("clone()") 151 | void copy() { 152 | var copy = cursor.clone(); 153 | assertNotSame(cursor, copy); 154 | assertTrue(copy.gotoFirstChild()); 155 | assertNotEquals(cursor.getCurrentNode(), copy.getCurrentNode()); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/test/java/io/github/treesitter/jtreesitter/TreeTest.java: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.jtreesitter; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | 5 | import io.github.treesitter.jtreesitter.languages.TreeSitterJava; 6 | import java.util.List; 7 | import java.util.concurrent.ExecutionException; 8 | import java.util.concurrent.Executors; 9 | import org.junit.jupiter.api.*; 10 | 11 | class TreeTest { 12 | private static final String source = "class Foo {}"; 13 | private static Language language; 14 | private static Parser parser; 15 | private Tree tree; 16 | 17 | @BeforeAll 18 | static void beforeAll() { 19 | language = new Language(TreeSitterJava.language()); 20 | parser = new Parser(language); 21 | } 22 | 23 | @AfterAll 24 | static void afterAll() { 25 | parser.close(); 26 | } 27 | 28 | @BeforeEach 29 | void setUp() { 30 | tree = parser.parse(source).orElseThrow(); 31 | } 32 | 33 | @AfterEach 34 | void tearDown() { 35 | tree.close(); 36 | } 37 | 38 | @Test 39 | void getLanguage() { 40 | assertSame(language, tree.getLanguage()); 41 | } 42 | 43 | @Test 44 | void getText() { 45 | assertEquals(source, tree.getText()); 46 | } 47 | 48 | @Test 49 | void getRootNode() { 50 | assertEquals("program", tree.getRootNode().getType()); 51 | } 52 | 53 | @Test 54 | void getRootNodeWithOffset() { 55 | var node = tree.getRootNodeWithOffset(6, new Point(0, 6)); 56 | assertNotNull(node); 57 | assertEquals(source.substring(6), node.getText()); 58 | } 59 | 60 | @Test 61 | void getIncludedRanges() { 62 | assertIterableEquals(List.of(Range.DEFAULT), tree.getIncludedRanges()); 63 | } 64 | 65 | @Test 66 | void getChangedRanges() { 67 | tree.edit(new InputEdit(0, 0, 7, new Point(0, 0), new Point(0, 0), new Point(0, 7))); 68 | var newSource = "public %s".formatted(source); 69 | try (var newTree = parser.parse(newSource, tree).orElseThrow()) { 70 | var range = tree.getChangedRanges(newTree).getFirst(); 71 | assertEquals(7, range.endByte()); 72 | assertEquals(7, range.endPoint().column()); 73 | } 74 | } 75 | 76 | @Test 77 | void edit() { 78 | tree.edit(new InputEdit(9, 9, 10, new Point(0, 10), new Point(0, 9), new Point(0, 10))); 79 | assertNull(tree.getText()); 80 | } 81 | 82 | @Test 83 | void walk() { 84 | try (var cursor = tree.walk()) { 85 | assertEquals(tree.getRootNode(), cursor.getCurrentNode()); 86 | } 87 | } 88 | 89 | @Test 90 | @DisplayName("clone()") 91 | void copy() { 92 | try (var exec = Executors.newSingleThreadExecutor()) { 93 | var result = exec.submit(() -> { 94 | try (var copy = tree.clone()) { 95 | assertNotSame(tree, copy); 96 | assertEquals( 97 | tree.getRootNode().toString(), copy.getRootNode().toString()); 98 | } 99 | }); 100 | result.get(); 101 | } catch (InterruptedException | ExecutionException e) { 102 | fail(e); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/test/java/io/github/treesitter/jtreesitter/languages/TreeSitterJava.java: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.jtreesitter.languages; 2 | 3 | import java.lang.foreign.*; 4 | 5 | public final class TreeSitterJava { 6 | private static final ValueLayout VOID_PTR = 7 | ValueLayout.ADDRESS.withTargetLayout(MemoryLayout.sequenceLayout(Long.MAX_VALUE, ValueLayout.JAVA_BYTE)); 8 | private static final FunctionDescriptor FUNC_DESC = FunctionDescriptor.of(VOID_PTR); 9 | private static final Linker LINKER = Linker.nativeLinker(); 10 | private static final TreeSitterJava INSTANCE = new TreeSitterJava(); 11 | 12 | private final Arena arena = Arena.ofAuto(); 13 | private final SymbolLookup symbols = findLibrary(); 14 | 15 | /** 16 | * {@snippet lang=c : 17 | * const TSLanguage *tree_sitter_java() 18 | * } 19 | */ 20 | public static MemorySegment language() { 21 | return INSTANCE.call("tree_sitter_java"); 22 | } 23 | 24 | private SymbolLookup findLibrary() { 25 | try { 26 | var library = System.mapLibraryName("tree-sitter-java"); 27 | return SymbolLookup.libraryLookup(library, arena); 28 | } catch (IllegalArgumentException e) { 29 | return SymbolLookup.loaderLookup(); 30 | } 31 | } 32 | 33 | private static UnsatisfiedLinkError unresolved(String name) { 34 | return new UnsatisfiedLinkError("Unresolved symbol: %s".formatted(name)); 35 | } 36 | 37 | @SuppressWarnings("SameParameterValue") 38 | private MemorySegment call(String name) throws UnsatisfiedLinkError { 39 | var address = symbols.find(name).orElseThrow(() -> unresolved(name)); 40 | try { 41 | var function = LINKER.downcallHandle(address, FUNC_DESC); 42 | return (MemorySegment) function.invokeExact(); 43 | } catch (Throwable e) { 44 | throw new RuntimeException("Call to %s failed".formatted(name), e); 45 | } 46 | } 47 | } 48 | --------------------------------------------------------------------------------