├── .editorconfig ├── .gitattributes ├── .github ├── CONTRIBUTING.md ├── dependabot.yml ├── detekt.yml ├── scripts │ ├── build-jni.ps1 │ └── build-jni.sh └── workflows │ ├── ci.yml │ ├── docs.yml │ ├── lint.yml │ └── publish.yml ├── .gitignore ├── .gitmodules ├── .idea ├── conventionalCommit.json ├── conventionalCommit.xml ├── detekt.xml ├── ktlint-plugin.xml └── vcs.xml ├── LICENSE ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml ├── logo-icon.svg └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── ktreesitter-plugin ├── README.md ├── build.gradle.kts └── src │ └── main │ ├── java │ └── io │ │ └── github │ │ └── treesitter │ │ └── ktreesitter │ │ └── plugin │ │ ├── GrammarExtension.java │ │ ├── GrammarFilesTask.java │ │ └── GrammarPlugin.java │ └── resources │ ├── CMakeLists.txt.in │ ├── android.kt.in │ ├── common.kt.in │ ├── interop.def.in │ ├── jni.c.in │ ├── jvm.kt.in │ └── native.kt.in ├── ktreesitter ├── .clang-format ├── CMakeLists.txt ├── README.md ├── build.gradle.kts └── src │ ├── androidInstrumentedTest │ ├── kotlin │ │ └── io │ │ │ └── github │ │ │ └── treesitter │ │ │ └── ktreesitter │ │ │ ├── LanguageTest.kt │ │ │ ├── LookaheadIteratorTest.kt │ │ │ ├── NodeTest.kt │ │ │ ├── ParserTest.kt │ │ │ ├── QueryTest.kt │ │ │ ├── TreeCursorTest.kt │ │ │ └── TreeTest.kt │ └── resources │ │ └── kotest.properties │ ├── androidMain │ └── kotlin │ │ └── io │ │ └── github │ │ └── treesitter │ │ └── ktreesitter │ │ ├── Language.kt │ │ ├── LookaheadIterator.kt │ │ ├── Node.kt │ │ ├── Parser.kt │ │ ├── Query.kt │ │ ├── RefCleaner.kt │ │ ├── Tree.kt │ │ └── TreeCursor.kt │ ├── commonMain │ └── kotlin │ │ └── io │ │ └── github │ │ └── treesitter │ │ └── ktreesitter │ │ ├── InputEdit.kt │ │ ├── Language.kt │ │ ├── LookaheadIterator.kt │ │ ├── Node.kt │ │ ├── Parser.kt │ │ ├── Point.kt │ │ ├── Query.kt │ │ ├── QueryCapture.kt │ │ ├── QueryError.kt │ │ ├── QueryMatch.kt │ │ ├── QueryPredicate.kt │ │ ├── QueryPredicateArg.kt │ │ ├── Range.kt │ │ ├── Tree.kt │ │ ├── TreeCursor.kt │ │ └── TreeSitter.kt │ ├── commonTest │ ├── kotlin │ │ └── io │ │ │ └── github │ │ │ └── treesitter │ │ │ └── ktreesitter │ │ │ ├── LanguageTest.kt │ │ │ ├── LookaheadIteratorTest.kt │ │ │ ├── NodeTest.kt │ │ │ ├── ParserTest.kt │ │ │ ├── QueryTest.kt │ │ │ ├── TreeCursorTest.kt │ │ │ └── TreeTest.kt │ └── resources │ │ └── kotest.properties │ ├── jni │ ├── language.c │ ├── lookahead_iterator.c │ ├── module.c │ ├── node.c │ ├── parser.c │ ├── query.c │ ├── tree.c │ ├── tree_cursor.c │ └── utils.h │ ├── jvmMain │ └── kotlin │ │ └── io │ │ └── github │ │ └── treesitter │ │ └── ktreesitter │ │ ├── Language.kt │ │ ├── LookaheadIterator.kt │ │ ├── NativeUtils.kt │ │ ├── Node.kt │ │ ├── Parser.kt │ │ ├── Query.kt │ │ ├── RefCleaner.kt │ │ ├── Tree.kt │ │ └── TreeCursor.kt │ ├── nativeInterop │ └── cinterop │ │ └── treesitter.def │ └── nativeMain │ └── kotlin │ └── io │ └── github │ └── treesitter │ └── ktreesitter │ ├── Language.kt │ ├── LookaheadIterator.kt │ ├── Node.kt │ ├── Parser.kt │ ├── Query.kt │ ├── Tree.kt │ ├── TreeCursor.kt │ └── Utils.kt ├── languages └── java │ └── build.gradle.kts └── settings.gradle.kts /.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 = 100 11 | 12 | [LICENSE] 13 | max_line_length = off 14 | 15 | [README.md] 16 | indent_size = 2 17 | max_line_length = off 18 | 19 | [.editorconfig] 20 | max_line_length = off 21 | 22 | [*.{json,yml,toml,xml}] 23 | indent_size = 2 24 | 25 | [*.{bat,ps1}] 26 | end_of_line = crlf 27 | 28 | # noinspection EditorConfigKeyCorrectness 29 | [*.{kt,kts}] 30 | ktlint_code_style = android_studio 31 | ktlint_standard_filename = disabled 32 | ktlint_standard_annotation = disabled 33 | ktlint_standard_class-signature = disabled 34 | ktlint_standard_function-naming = disabled 35 | ktlint_standard_multiline-if-else = disabled 36 | ij_formatter_tags_enabled = true 37 | ij_kotlin_imports_layout = * 38 | ij_kotlin_allow_trailing_comma = false 39 | ij_kotlin_allow_trailing_comma_on_call_site = false 40 | ij_kotlin_name_count_to_use_star_import = 10 41 | ij_kotlin_name_count_to_use_star_import_for_members = 10 42 | ij_kotlin_packages_to_use_import_on_demand = kotlinx.cinterop.*,io.kotest.matchers.**,io.github.treesitter.ktreesitter.internal.* 43 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | 3 | /gradlew linguist-generated 4 | /gradlew.bat linguist-generated eol=crlf 5 | /.github/scripts/build-jni.ps1 eol=crlf 6 | /gradle/wrapper/gradle-wrapper.jar binary 7 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Commits 4 | 5 | Commits must follow the [Conventional Commits] specification. 6 | If you're developing on IntelliJ IDEA or Android Studio, 7 | you can use the [Conventional Commit plugin]. 8 | 9 | [Conventional Commits]: https://www.conventionalcommits.org/en/v1.0.0/ 10 | [Conventional Commit plugin]: https://plugins.jetbrains.com/plugin/13389-conventional-commit 11 | 12 | ## Prerequisites 13 | 14 | - JDK 17+ 15 | - Android SDK & NDK 16 | - CMake 17 | - C compiler 18 | 19 | ## Building 20 | 21 | ### Clone the repository 22 | 23 | ```shell 24 | git clone https://github.com/tree-sitter/kotlin-tree-sitter 25 | cd kotlin-tree-sitter 26 | ``` 27 | 28 | ### Build the JNI libraries 29 | 30 | *This step is only necessary for JVM or Android development.* 31 | 32 | Generate the grammar files: 33 | 34 | ```shell 35 | ./gradlew generateGrammarFiles 36 | ``` 37 | 38 | Set the `CMAKE_INSTALL_LIBDIR` environment variable. 39 | The value depends on your system: 40 | 41 | - `lib/linux/x64` 42 | - `lib/linux/aarch64` 43 | - `lib/macos/x64` 44 | - `lib/macos/aarch64` 45 | - `lib/windows/x64` 46 | 47 | Build the libraries (change `.sh` to `.ps1` on Windows): 48 | 49 | ```shell 50 | ./.github/scripts/build-jni.sh 51 | ``` 52 | 53 | ### Run the tests 54 | 55 | #### JVM 56 | 57 | ```shell 58 | ./gradlew :ktreesitter:jvmTest 59 | ``` 60 | 61 | #### Android 62 | 63 | *Requires a connected device.* 64 | 65 | ```shell 66 | ./gradlew :ktreesitter:connectedDebugAndroidTest 67 | ``` 68 | 69 | #### Native 70 | 71 | Linux: 72 | 73 | ```shell 74 | ./gradlew :ktreesitter:linuxX64Test 75 | ``` 76 | 77 | macOS: 78 | 79 | ```shell 80 | # x64 81 | ./gradlew :ktreesitter:macosX64Test 82 | # arm64 83 | ./gradlew :ktreesitter:macosArm64Test 84 | ``` 85 | 86 | Windows: 87 | 88 | ```shell 89 | ./gradlew :ktreesitter:mingwX64Test 90 | ``` 91 | 92 | ## Linting 93 | 94 | Code linting is performed using [Ktlint] and [detekt]. 95 | Configuration for IntelliJ IDEA and Android Studio is included 96 | (requires the [Ktlint][Ktlint plugin] and [detekt][detekt plugin] plugins). 97 | 98 | [Ktlint]: https://pinterest.github.io/ktlint/latest/ 99 | [detekt]: https://detekt.dev/ 100 | [Ktlint plugin]: https://plugins.jetbrains.com/plugin/15057-ktlint 101 | [detekt plugin]: https://plugins.jetbrains.com/plugin/10761-detekt 102 | -------------------------------------------------------------------------------- /.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: gradle 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 | groups: 39 | gradle: 40 | patterns: ["*"] 41 | exclude-patterns: 42 | - android-gradle 43 | -------------------------------------------------------------------------------- /.github/detekt.yml: -------------------------------------------------------------------------------- 1 | complexity: 2 | TooManyFunctions: 3 | active: false 4 | LongParameterList: 5 | functionThreshold: 7 6 | 7 | performance: 8 | SpreadOperator: 9 | excludes: 10 | - "**/*.kts" 11 | 12 | style: 13 | WildcardImport: 14 | active: false 15 | ForbiddenComment: 16 | active: false 17 | UnusedPrivateProperty: 18 | excludes: 19 | - "**/commonMain/**" 20 | MagicNumber: 21 | ignoreNumbers: 22 | - "-1" 23 | - "0" 24 | - "1" 25 | - "2" 26 | - "3" 27 | - "10" 28 | - "16" 29 | - "0x7F" 30 | ReturnCount: 31 | max: 3 32 | -------------------------------------------------------------------------------- /.github/scripts/build-jni.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env pwsh 2 | 3 | & cmake -S ktreesitter -B ktreesitter/.cmake/build ` 4 | -DCMAKE_VERBOSE_MAKEFILE=ON ` 5 | -DCMAKE_INSTALL_PREFIX=ktreesitter/src/jvmMain/resources ` 6 | -DCMAKE_INSTALL_BINDIR="$env:CMAKE_INSTALL_LIBDIR" 7 | & cmake --build ktreesitter/.cmake/build --config Debug 8 | & cmake --install ktreesitter/.cmake/build --config Debug 9 | 10 | foreach ($dir in Get-ChildItem -Directory -Path languages) { 11 | & cmake -S "$dir/build/generated" -B "$dir/.cmake/build" ` 12 | -DCMAKE_INSTALL_PREFIX="$dir/build/generated/src/jvmMain/resources" ` 13 | -DCMAKE_INSTALL_BINDIR="$env:CMAKE_INSTALL_LIBDIR" 14 | & cmake --build "$dir/.cmake/build" --config Debug 15 | & cmake --install "$dir/.cmake/build" --config Debug 16 | } 17 | -------------------------------------------------------------------------------- /.github/scripts/build-jni.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | 3 | cmake -S ktreesitter -B ktreesitter/.cmake/build \ 4 | -DCMAKE_BUILD_TYPE=RelWithDebugInfo \ 5 | -DCMAKE_VERBOSE_MAKEFILE=ON \ 6 | -DCMAKE_OSX_ARCHITECTURES=arm64 \ 7 | -DCMAKE_INSTALL_PREFIX=ktreesitter/src/jvmMain/resources \ 8 | -DCMAKE_INSTALL_LIBDIR="$CMAKE_INSTALL_LIBDIR" 9 | cmake --build ktreesitter/.cmake/build 10 | cmake --install ktreesitter/.cmake/build 11 | 12 | for dir in languages/*/; do 13 | cmake -S "${dir}build/generated" -B "${dir}.cmake/build" \ 14 | -DCMAKE_BUILD_TYPE=RelWithDebugInfo \ 15 | -DCMAKE_OSX_ARCHITECTURES=arm64 \ 16 | -DCMAKE_INSTALL_PREFIX="${dir}build/generated/src/jvmMain/resources" \ 17 | -DCMAKE_INSTALL_LIBDIR="$CMAKE_INSTALL_LIBDIR" 18 | cmake --build "${dir}.cmake/build" 19 | cmake --install "${dir}.cmake/build" 20 | done 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | paths: 7 | - "**/*.kt" 8 | - "**/*.kts" 9 | - "**/jni/*" 10 | - gradle/** 11 | - gradle.properties 12 | pull_request: 13 | paths: 14 | - "/*.kt" 15 | - "**/*.kts" 16 | - "**/jni/*" 17 | - gradle/** 18 | - gradle.properties 19 | 20 | concurrency: 21 | cancel-in-progress: true 22 | group: ${{github.workflow}}-${{github.ref_name}} 23 | 24 | permissions: 25 | contents: write 26 | 27 | jobs: 28 | generate: 29 | runs-on: ubuntu-latest 30 | name: Generate grammar files 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: 17 41 | cache: gradle 42 | cache-dependency-path: | 43 | gradle/libs.versions.toml 44 | gradle/wrapper/gradle-wrapper.properties 45 | - name: Cache Kotlin/Native prebuilt 46 | uses: actions/cache@v4 47 | with: 48 | path: ${{runner.tool_cache}}/konan/kotlin-native-prebuilt-* 49 | key: konan-${{runner.os}}-prebuilt-1.9 50 | - name: Generate files 51 | run: ./gradlew --no-daemon generateGrammarFiles 52 | env: 53 | KONAN_DATA_DIR: ${{runner.tool_cache}}/konan 54 | - name: Upload artifact 55 | uses: actions/upload-artifact@v4 56 | with: 57 | name: generated-files 58 | path: languages/*/build/generated/** 59 | retention-days: 1 60 | test: 61 | runs-on: ${{matrix.os}} 62 | name: >- 63 | Test ${{matrix.platform}} platform 64 | ${{matrix.lib_platform && format('({0}-{1})', matrix.lib_platform, matrix.lib_arch)}} 65 | needs: [generate] 66 | strategy: 67 | fail-fast: false 68 | matrix: 69 | include: 70 | - os: ubuntu-latest 71 | platform: JVM 72 | targets: :ktreesitter:jvmTest 73 | lib_platform: linux 74 | lib_arch: x64 75 | - os: windows-latest 76 | platform: JVM 77 | targets: :ktreesitter:jvmTest 78 | lib_platform: windows 79 | lib_arch: x64 80 | - os: macos-latest 81 | platform: JVM 82 | targets: :ktreesitter:jvmTest 83 | lib_platform: macos 84 | lib_arch: aarch64 85 | - os: ubuntu-latest 86 | platform: Android 87 | targets: :ktreesitter:assembleDebug 88 | - os: ubuntu-latest 89 | platform: Linux 90 | targets: >- 91 | :ktreesitter:compileKotlinLinuxArm64 92 | :ktreesitter:linuxX64Test 93 | - os: windows-latest 94 | platform: Windows 95 | targets: :ktreesitter:mingwX64Test 96 | - os: macos-latest 97 | platform: macOS/iOS 98 | targets: >- 99 | :ktreesitter:macosX64Test 100 | :ktreesitter:macosArm64Test 101 | :ktreesitter:iosSimulatorArm64Test 102 | steps: 103 | - name: Checkout repository 104 | uses: actions/checkout@v4 105 | with: 106 | submodules: true 107 | - name: Set up Java 108 | uses: actions/setup-java@v4 109 | with: 110 | distribution: temurin 111 | java-version: 17 112 | cache: gradle 113 | cache-dependency-path: | 114 | gradle/libs.versions.toml 115 | gradle/wrapper/gradle-wrapper.properties 116 | - name: Set up cross compilation 117 | run: sudo apt-get install -qy {binutils,gcc}-aarch64-linux-gnu 118 | if: matrix.platform == 'Linux' 119 | - name: Restore Kotlin/Native prebuilt 120 | uses: actions/cache/restore@v4 121 | with: 122 | path: ${{runner.tool_cache}}/konan/kotlin-native-prebuilt-* 123 | key: konan-${{runner.os}}-prebuilt-1.9 124 | - name: Download generated files 125 | uses: actions/download-artifact@v4 126 | with: 127 | path: languages 128 | name: generated-files 129 | - name: Build JNI libraries 130 | if: matrix.platform == 'JVM' 131 | run: .github/scripts/build-jni.${{matrix.os == 'windows-latest' && 'ps1' || 'sh'}} 132 | env: 133 | CMAKE_INSTALL_LIBDIR: lib/${{matrix.lib_platform}}/${{matrix.lib_arch}} 134 | - name: Cache Kotlin/Native dependencies 135 | id: cache-dependencies 136 | uses: actions/cache@v4 137 | if: matrix.platform != 'JVM' && matrix.platform != 'Android' 138 | with: 139 | path: ${{runner.tool_cache}}/konan/dependencies 140 | key: konan-${{runner.os}}-dependencies 141 | - name: Download Kotlin/Native dependencies 142 | if: matrix.platform == 'macOS/iOS' && steps.cache-dependencies.outputs.cache-hit != 'true' 143 | run: |- 144 | mkdir -p "$RUNNER_TOOL_CACHE/konan/dependencies" 145 | curl -LSs https://download-cdn.jetbrains.com/kotlin/native/$DEP.tar.gz | \ 146 | tar -xzf - -C "$RUNNER_TOOL_CACHE/konan/dependencies" 147 | env: 148 | DEP: apple-llvm-20200714-macos-aarch64-essentials 149 | - name: Set up Ninja 150 | if: matrix.platform == 'Android' 151 | run: |- 152 | sudo apt-get update 153 | sudo apt-get install -y ninja-build 154 | - name: Run tests 155 | run: ./gradlew --no-daemon ${{matrix.targets}} 156 | env: 157 | KONAN_DATA_DIR: ${{runner.tool_cache}}/konan 158 | - name: Report test results 159 | uses: mikepenz/action-junit-report@v5 160 | if: matrix.platform == 'JVM' && !cancelled() 161 | with: 162 | annotate_only: true 163 | detailed_summary: true 164 | report_paths: ktreesitter/build/reports/xml/* 165 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Docs 2 | 3 | run-name: Update API docs 4 | 5 | on: 6 | workflow_run: 7 | workflows: [CI] 8 | types: [completed] 9 | branches: [master] 10 | push: 11 | branches: [master] 12 | paths: 13 | - ktreesitter/README.md 14 | 15 | concurrency: 16 | cancel-in-progress: true 17 | group: ${{github.workflow}}-${{github.ref_name}} 18 | 19 | permissions: 20 | pages: write 21 | id-token: write 22 | 23 | jobs: 24 | docs: 25 | runs-on: ubuntu-latest 26 | name: Publish docs on GitHub pages 27 | if: >- 28 | github.event_name == 'push' || 29 | github.event.workflow_run.conclusion == 'success' 30 | environment: 31 | name: github-pages 32 | url: ${{steps.deployment.outputs.page_url}} 33 | steps: 34 | - name: Checkout repository 35 | uses: actions/checkout@v4 36 | with: 37 | submodules: true 38 | - name: Set up Java 39 | uses: actions/setup-java@v4 40 | with: 41 | distribution: temurin 42 | java-version: 17 43 | cache: gradle 44 | cache-dependency-path: | 45 | gradle/libs.versions.toml 46 | gradle/wrapper/gradle-wrapper.properties 47 | - name: Restore Kotlin/Native prebuilt 48 | uses: actions/cache/restore@v4 49 | with: 50 | path: ${{runner.tool_cache}}/konan/kotlin-native-prebuilt-* 51 | key: konan-${{runner.os}}-prebuilt-1.9 52 | - name: Restore Kotlin/Native dependencies 53 | uses: actions/cache/restore@v4 54 | with: 55 | path: ${{runner.tool_cache}}/konan/dependencies 56 | key: konan-${{runner.os}}-dependencies 57 | - name: Set up Ninja 58 | run: |- 59 | sudo apt-get update 60 | sudo apt-get install -y ninja-build 61 | - name: Build documentation 62 | run: ./gradlew --no-daemon generateFiles :ktreesitter:dokkaHtml 63 | env: 64 | KONAN_DATA_DIR: ${{runner.tool_cache}}/konan 65 | - name: Upload pages artifact 66 | uses: actions/upload-pages-artifact@v3 67 | with: 68 | path: ktreesitter/build/dokka/html 69 | - name: Deploy to GitHub Pages 70 | id: deployment 71 | uses: actions/deploy-pages@v4 72 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | paths: 7 | - "**/*.kt" 8 | - "**.kts" 9 | - .github/workflows/lint.yml 10 | pull_request: 11 | paths: 12 | - "**/*.kt" 13 | - "**.kts" 14 | - .github/workflows/lint.yml 15 | 16 | concurrency: 17 | cancel-in-progress: true 18 | group: ${{github.workflow}}-${{github.ref_name}} 19 | 20 | permissions: 21 | contents: read 22 | security-events: write 23 | 24 | jobs: 25 | ktlint: 26 | runs-on: ubuntu-latest 27 | steps: 28 | - name: Checkout repository 29 | uses: actions/checkout@v4 30 | - name: Set up Java 31 | uses: actions/setup-java@v4 32 | with: 33 | distribution: temurin 34 | java-version: 17 35 | - name: Install ktlint 36 | run: |- 37 | curl -sSLO https://github.com/pinterest/ktlint/releases/download/1.3.1/ktlint 38 | chmod a+x ktlint && mv ktlint $RUNNER_TOOL_CACHE/ktlint 39 | - name: Run ktlint 40 | id: ktlint 41 | run: $RUNNER_TOOL_CACHE/ktlint --reporter=sarif,output=build/reports/ktlint.sarif 42 | - name: Upload report 43 | uses: github/codeql-action/upload-sarif@v3 44 | if: "!cancelled() && steps.ktlint.outcome == 'failure'" 45 | with: 46 | sarif_file: build/reports/ktlint.sarif 47 | # FIXME: github/codeql-action#2215 48 | checkout_path: ${{env.HOME}} 49 | category: ktlint 50 | detekt: 51 | runs-on: ubuntu-latest 52 | steps: 53 | - name: Checkout repository 54 | uses: actions/checkout@v4 55 | - name: Set up Java 56 | uses: actions/setup-java@v4 57 | with: 58 | distribution: temurin 59 | java-version: 17 60 | - name: Install detekt 61 | run: |- 62 | curl -sSLO https://github.com/detekt/detekt/releases/download/v1.23.7/detekt-cli-1.23.7-all.jar 63 | mv detekt-cli-1.23.7-all.jar $RUNNER_TOOL_CACHE/detekt-cli.jar 64 | - name: Run detekt 65 | id: detekt 66 | run: >- 67 | java -jar $RUNNER_TOOL_CACHE/detekt-cli.jar --build-upon-default-config 68 | --jvm-target 17 -c .github/detekt.yml -r sarif:build/reports/detekt.sarif 69 | - name: Upload report 70 | uses: github/codeql-action/upload-sarif@v3 71 | if: "!cancelled() && steps.detekt.outcome == 'failure'" 72 | with: 73 | sarif_file: build/reports/detekt.sarif 74 | category: detekt 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Gradle ### 2 | .gradle/ 3 | build/ 4 | kotlin-js-store/ 5 | dependency-graph-reports/ 6 | local.properties 7 | 8 | ### C ### 9 | .cache/ 10 | .cmake/ 11 | *.a 12 | *.lib 13 | *.so 14 | *.dylib 15 | *.dll 16 | 17 | ### JetBrains ### 18 | .idea/* 19 | out/ 20 | *.iws 21 | *.iml 22 | *.ipr 23 | *.trace 24 | *.log 25 | 26 | !.idea/conventionalCommit.json 27 | !.idea/conventionalCommit.xml 28 | !.idea/detekt.xml 29 | !.idea/ktlint-plugin.xml 30 | !.idea/vcs.xml 31 | 32 | ### Vim ### 33 | [._]*.s[a-v][a-z] 34 | [._]*.sw[a-p] 35 | [._]s[a-rt-v][a-z] 36 | [._]ss[a-gi-z] 37 | [._]sw[a-p] 38 | Session.vim 39 | .nvimrc 40 | tags 41 | *~ 42 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "tree-sitter"] 2 | path = tree-sitter 3 | url = https://github.com/tree-sitter/tree-sitter 4 | branch = release-0.24 5 | 6 | [submodule "tree-sitter-java"] 7 | path = languages/java/tree-sitter-java 8 | url = https://github.com/tree-sitter/tree-sitter-java 9 | shallow = true 10 | -------------------------------------------------------------------------------- /.idea/conventionalCommit.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://github.com/lppedd/idea-conventional-commit/raw/master/src/main/resources/defaults/conventionalcommit.schema.json", 3 | "types": { 4 | "refactor": { 5 | "description": "Changes which neither fix a bug nor add a feature" 6 | }, 7 | "fix": { 8 | "description": "Changes which patch a bug" 9 | }, 10 | "feat": { 11 | "description": "Changes which introduce a new feature" 12 | }, 13 | "build": { 14 | "description": "Changes which affect the build system or external dependencies" 15 | }, 16 | "chore": { 17 | "description": "Changes which are not user-facing" 18 | }, 19 | "test": { 20 | "description": "Changes which add missing tests or fix existing ones" 21 | }, 22 | "docs": { 23 | "description": "Changes which affect documentation" 24 | }, 25 | "ci": { 26 | "description": "Changes which affect CI configuration files and scripts" 27 | }, 28 | "revert": { 29 | "description": "Changes which revert a previous commit" 30 | } 31 | }, 32 | "commonScopes": { 33 | "android": {}, 34 | "jvm": {}, 35 | "native": {}, 36 | "jni": {}, 37 | "plugin": {}, 38 | "languages": {} 39 | }, 40 | "footerTypes": [ 41 | { 42 | "name": "Closes", 43 | "description": "The commit closes issues or pull requests" 44 | }, 45 | { 46 | "name": "Co-authored-by", 47 | "description": "The specified person co-authored the commit's changes" 48 | }, 49 | { 50 | "name": "Refs", 51 | "description": "The commit references another commit by its hash" 52 | } 53 | ] 54 | } 55 | -------------------------------------------------------------------------------- /.idea/conventionalCommit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/detekt.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/ktlint-plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | DISTRACT_FREE 5 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /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 | # Kotlin Tree-sitter 2 | 3 | [![CI][ci]](https://github.com/tree-sitter/kotlin-tree-sitter/actions/workflows/ci.yml) 4 | [![docs][docs]](https://tree-sitter.github.io/kotlin-tree-sitter/) 5 | 6 | Kotlin bindings to the [tree-sitter] parsing library. 7 | 8 | ## Modules 9 | 10 | ### ktreesitter 11 | 12 | [![central][central]](https://central.sonatype.com/artifact/io.github.tree-sitter/ktreesitter) 13 | 14 | The KTreeSitter library module. 15 | 16 | ### ktreesitter-plugin 17 | 18 | [![portal][portal]](https://plugins.gradle.org/plugin/io.github.tree-sitter.ktreesitter-plugin) 19 | 20 | The plugin responsible for generating source files for languages. 21 | 22 | ### languages 23 | 24 | Some bundled languages that are relevant to Kotlin development. 25 | 26 | [tree-sitter]: https://tree-sitter.github.io/tree-sitter/ 27 | [ci]: https://img.shields.io/github/actions/workflow/status/tree-sitter/kotlin-tree-sitter/ci.yml?logo=github&label=CI 28 | [central]: https://img.shields.io/maven-central/v/io.github.tree-sitter/ktreesitter?logo=sonatype&label=Maven%20Central 29 | [portal]: https://img.shields.io/gradle-plugin-portal/v/io.github.tree-sitter.ktreesitter-plugin?logo=gradle&label=Gradle%20Plugin%20Portal 30 | [docs]: https://img.shields.io/github/deployments/tree-sitter/kotlin-tree-sitter/github-pages?logo=kotlin&label=API%20Docs 31 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.kotlin.mpp) apply false 3 | alias(libs.plugins.android.library) apply false 4 | alias(libs.plugins.kotest) apply false 5 | alias(libs.plugins.dokka) apply false 6 | } 7 | 8 | allprojects { 9 | repositories { 10 | google() 11 | mavenCentral() 12 | } 13 | } 14 | 15 | subprojects { 16 | group = "io.github.tree-sitter" 17 | } 18 | 19 | tasks.wrapper { 20 | gradleVersion = "8.7" 21 | distributionType = Wrapper.DistributionType.BIN 22 | } 23 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # The version of the project. 2 | project.version=0.24.1 3 | 4 | # The Android SDK version that is used to compile the project. 5 | sdk.version.compile=34 6 | # The minimum supported Android SDK version. 7 | sdk.version.min=23 8 | # The Android NDK version that is used to compile the project. 9 | ndk.version=26.3.11579264 10 | # The CMake version that is used to compile the project. 11 | cmake.version=3.31.3 12 | 13 | # Specifies the JVM arguments used for the daemon process. 14 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 15 | # Disables deprecation warnings. 16 | org.gradle.warning.mode=none 17 | # Hides the welcome message. 18 | org.gradle.welcome=never 19 | 20 | # Specifies the Kotlin code style for this project. 21 | kotlin.code.style=official 22 | # Enables the new Android multiplatform sourceset layout. 23 | kotlin.mpp.androidSourceSetLayoutVersion=2 24 | # Enable C interop sharing 25 | kotlin.mpp.enableCInteropCommonization=true 26 | # Disable default stdlib dependency 27 | kotlin.stdlib.default.dependency=false 28 | # Ignore disabled Kotlin/Native targets 29 | kotlin.native.ignoreDisabledTargets=true 30 | 31 | # Enables the AndroidX package structure. 32 | android.useAndroidX=true 33 | # Disables automatically downloading the Android SDK. 34 | android.builder.sdkDownload=false 35 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | kotlin-stdlib = "[1.9,2.0)" 3 | #noinspection GradleDependency 4 | android-gradle = {strictly = "8.2.0"} 5 | kotest = "5.9.1" 6 | dokka = "1.9.20" 7 | 8 | [libraries.kotlin-stdlib] 9 | module = "org.jetbrains.kotlin:kotlin-stdlib" 10 | version.ref = "kotlin-stdlib" 11 | 12 | [libraries.kotest-engine] 13 | module = "io.kotest:kotest-framework-engine" 14 | version.ref = "kotest" 15 | 16 | [libraries.kotest-assertions] 17 | module = "io.kotest:kotest-assertions-core" 18 | version.ref = "kotest" 19 | 20 | [libraries.kotest-junit-runner] 21 | module = "io.kotest:kotest-runner-junit5" 22 | version.ref = "kotest" 23 | 24 | [libraries.kotest-junit-reporter] 25 | module = "io.kotest:kotest-extensions-junitxml" 26 | version.ref = "kotest" 27 | 28 | [libraries.kotest-android-runner] 29 | module = "br.com.colman:kotest-runner-android" 30 | version = "1.1.1" 31 | 32 | [libraries.androidx-test-runner] 33 | module = "androidx.test:runner" 34 | version = "1.5.2" 35 | 36 | [libraries.dokka-base] 37 | module = "org.jetbrains.dokka:dokka-base" 38 | version.ref = "dokka" 39 | 40 | [plugins.kotlin-mpp] 41 | id = "org.jetbrains.kotlin.multiplatform" 42 | version.ref = "kotlin-stdlib" 43 | 44 | [plugins.android-library] 45 | id = "com.android.library" 46 | version.ref = "android-gradle" 47 | 48 | [plugins.kotest] 49 | id = "io.kotest.multiplatform" 50 | version.ref = "kotest" 51 | 52 | [plugins.dokka] 53 | id = "org.jetbrains.dokka" 54 | version.ref = "dokka" 55 | 56 | [plugins.gradle-publish] 57 | id = "com.gradle.plugin-publish" 58 | version = "1.2.1" 59 | 60 | [bundles] 61 | kotest-core = [ 62 | "kotest-engine", 63 | "kotest-assertions", 64 | ] 65 | kotest-junit = [ 66 | "kotest-junit-runner", 67 | "kotest-junit-reporter", 68 | ] 69 | kotest-android = [ 70 | "kotest-android-runner", 71 | "androidx-test-runner", 72 | ] 73 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tree-sitter/kotlin-tree-sitter/a1a9e0e589af3bc16a9b6d0046732e9f396111bc/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /ktreesitter-plugin/README.md: -------------------------------------------------------------------------------- 1 | # KTreeSitter plugin 2 | 3 | A plugin that generates code for KTreeSitter grammar packages. 4 | 5 | ## Installation 6 | 7 | ```groovy 8 | buildscript { 9 | repositories { 10 | gradlePluginPortal() 11 | } 12 | } 13 | 14 | plugins { 15 | id("io.github.tree-sitter.ktreesitter-plugin") 16 | } 17 | ``` 18 | 19 | ## Configuration 20 | 21 | ```groovy 22 | grammar { 23 | /* Default options */ 24 | // The base directory of the grammar 25 | baseDir = projectDir.parentFile.parentFile 26 | // The name of the C interop def file 27 | interopName = "grammar" 28 | // The name of the JNI library 29 | libraryName = "ktreesitter-$grammarName" 30 | 31 | /* Required options */ 32 | // The name of the grammar 33 | grammarName = "java" 34 | // The name of the class 35 | className = "TreeSitterJava" 36 | // The name of the package 37 | packageName = "io.github.treesitter.ktreesitter.java" 38 | // The source files of the grammar 39 | files = arrayOf( 40 | baseDir.get().resolve("src/parser.c"), 41 | // baseDir.get().resolve("src/scanner.c") 42 | ) 43 | } 44 | ``` 45 | -------------------------------------------------------------------------------- /ktreesitter-plugin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import java.util.Properties 2 | 3 | group = "io.github.tree-sitter" 4 | version = with(Properties()) { 5 | file("../gradle.properties").reader().use(::load) 6 | getProperty("project.version") 7 | } 8 | 9 | plugins { 10 | `java-gradle-plugin` 11 | id("com.gradle.plugin-publish") version "1.2.1" 12 | } 13 | 14 | repositories { 15 | mavenCentral() 16 | } 17 | 18 | java { 19 | sourceCompatibility = JavaVersion.VERSION_17 20 | targetCompatibility = JavaVersion.VERSION_17 21 | } 22 | 23 | @Suppress("UnstableApiUsage") 24 | gradlePlugin { 25 | vcsUrl = "https://github.com/tree-sitter/kotlin-tree-sitter" 26 | website = "https://github.com/tree-sitter/kotlin-tree-sitter/tree/master/ktreesitter-plugin" 27 | plugins.create("ktreesitter") { 28 | id = "$group.${project.name}" 29 | displayName = "KTreeSitter grammar plugin" 30 | description = "A plugin that generates code for KTreeSitter grammar packages" 31 | implementationClass = "io.github.treesitter.ktreesitter.plugin.GrammarPlugin" 32 | tags = listOf("tree-sitter", "code", "generator") 33 | } 34 | } 35 | 36 | tasks.javadoc { 37 | (options as CoreJavadocOptions).addBooleanOption("Xdoclint:all,-missing", true) 38 | } 39 | 40 | tasks.validatePlugins { 41 | enableStricterValidation.set(true) 42 | } 43 | -------------------------------------------------------------------------------- /ktreesitter-plugin/src/main/java/io/github/treesitter/ktreesitter/plugin/GrammarExtension.java: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter.plugin; 2 | 3 | import java.io.File; 4 | import org.gradle.api.provider.MapProperty; 5 | import org.gradle.api.provider.Property; 6 | 7 | /** The grammar configuration extension. */ 8 | public interface GrammarExtension { 9 | /** 10 | * The base directory of the grammar. 11 | * 12 | *

Default: {@code ../..}

13 | */ 14 | Property getBaseDir(); 15 | /** 16 | * The name of the grammar. 17 | * 18 | *

Required

19 | */ 20 | Property getGrammarName(); 21 | /** 22 | * The source files of the grammar. 23 | * 24 | *

Required

25 | */ 26 | Property getFiles(); 27 | /** 28 | * The name of the C interop def file. 29 | * 30 | *

Default: {@code grammar}

31 | */ 32 | Property getInteropName(); 33 | /** 34 | * The name of the JNI library. 35 | * 36 | *

Default: {@code ktreesitter-${grammarName}}

37 | */ 38 | Property getLibraryName(); 39 | /** 40 | * The name of the package. 41 | * 42 | *

Required

43 | */ 44 | Property getPackageName(); 45 | /** 46 | * The name of the class. 47 | * 48 | *

Required

49 | */ 50 | Property getClassName(); 51 | /** 52 | * A map of Java methods to C functions. 53 | * 54 | *

Default: {@code language -> tree_sitter_${grammarName}}

55 | */ 56 | MapProperty getLanguageMethods(); 57 | } 58 | -------------------------------------------------------------------------------- /ktreesitter-plugin/src/main/java/io/github/treesitter/ktreesitter/plugin/GrammarPlugin.java: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter.plugin; 2 | 3 | import java.util.Map; 4 | 5 | import org.gradle.api.NonNullApi; 6 | import org.gradle.api.Plugin; 7 | import org.gradle.api.Project; 8 | 9 | /** 10 | * A plugin that generates code for 11 | * KTreeSitter 12 | * grammar packages. 13 | */ 14 | @NonNullApi 15 | public abstract class GrammarPlugin implements Plugin { 16 | @Override 17 | public void apply(Project project) { 18 | var extension = project.getExtensions().create("grammar", GrammarExtension.class); 19 | extension.getInteropName().convention("grammar"); 20 | extension.getBaseDir().convention( 21 | project.getProjectDir().getParentFile().getParentFile() 22 | ); 23 | extension.getLibraryName().convention( 24 | extension.getGrammarName().map(name -> "ktreesitter-" + name) 25 | ); 26 | extension.getLanguageMethods().convention( 27 | extension.getGrammarName().map(name -> Map.of("language", "tree_sitter_" + name)) 28 | ); 29 | 30 | project.getTasks().register("generateGrammarFiles", GrammarFilesTask.class, it -> { 31 | it.setGrammarDir(extension.getBaseDir().get()); 32 | it.setGrammarName(extension.getGrammarName().get()); 33 | it.setGrammarFiles(extension.getFiles().get()); 34 | it.setInteropName(extension.getInteropName().get()); 35 | it.setLibraryName(extension.getLibraryName().get()); 36 | it.setPackageName(extension.getPackageName().get()); 37 | it.setClassName(extension.getClassName().get()); 38 | it.setLanguageMethods(extension.getLanguageMethods().get()); 39 | 40 | var generatedDir = project.getLayout().getBuildDirectory().get().dir("generated"); 41 | it.getGeneratedSrc().set(generatedDir.dir("src")); 42 | it.getCmakeListsFile().set(generatedDir.file("CMakeLists.txt")); 43 | it.getInteropFile().set( 44 | generatedDir.dir("src").dir("nativeInterop").file(it.getInteropName() + ".def") 45 | ); 46 | 47 | it.getOutputs().dir(it.getGeneratedSrc()); 48 | it.getOutputs().files(it.getCmakeListsFile(), it.getInteropFile()); 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /ktreesitter-plugin/src/main/resources/CMakeLists.txt.in: -------------------------------------------------------------------------------- 1 | # Automatically generated file. DO NOT MODIFY 2 | 3 | cmake_minimum_required(VERSION 3.12.0) 4 | 5 | project(ktreesitter-java LANGUAGES C) 6 | 7 | find_package(JNI REQUIRED) 8 | 9 | set(CMAKE_C_STANDARD 11) 10 | 11 | if(MSVC) 12 | add_compile_options(/W3 /wd4244) 13 | else(MSVC) 14 | set(CMAKE_C_VISIBILITY_PRESET hidden) 15 | add_compile_options(-Wall -Wextra 16 | -Wno-unused-parameter 17 | -Werror=implicit-function-declaration) 18 | endif(MSVC) 19 | 20 | include_directories(${JNI_INCLUDE_DIRS} @INCLUDE@) 21 | 22 | add_compile_definitions(TREE_SITTER_HIDE_SYMBOLS) 23 | 24 | add_library(@LIBRARY@ SHARED @SOURCES@) 25 | 26 | set_target_properties(@LIBRARY@ PROPERTIES DEFINE_SYMBOL "") 27 | 28 | install(TARGETS @LIBRARY@ ARCHIVE EXCLUDE_FROM_ALL) 29 | -------------------------------------------------------------------------------- /ktreesitter-plugin/src/main/resources/android.kt.in: -------------------------------------------------------------------------------- 1 | // Automatically generated file. DO NOT MODIFY 2 | 3 | package @PACKAGE@ 4 | 5 | import dalvik.annotation.optimization.CriticalNative 6 | import javax.annotation.processing.Generated 7 | 8 | @Suppress("FunctionName") 9 | @Generated("io.github.treesitter.ktreesitter-plugin") 10 | actual object @CLASS@ { 11 | init { 12 | System.loadLibrary("@LIBRARY@") 13 | } 14 | 15 | @METHODS@ 16 | } 17 | -------------------------------------------------------------------------------- /ktreesitter-plugin/src/main/resources/common.kt.in: -------------------------------------------------------------------------------- 1 | // Automatically generated file. DO NOT MODIFY 2 | 3 | package @PACKAGE@ 4 | 5 | expect object @CLASS@ { 6 | @METHODS@ 7 | } 8 | -------------------------------------------------------------------------------- /ktreesitter-plugin/src/main/resources/interop.def.in: -------------------------------------------------------------------------------- 1 | # Automatically generated file. DO NOT MODIFY 2 | 3 | package = @PACKAGE@.internal 4 | headers = tree-sitter-@GRAMMAR@.h 5 | compilerOpts = -DTREE_SITTER_HIDE_SYMBOLS 6 | staticLibraries = libtree-sitter-@GRAMMAR@.a 7 | -------------------------------------------------------------------------------- /ktreesitter-plugin/src/main/resources/jni.c.in: -------------------------------------------------------------------------------- 1 | // Automatically generated file. DO NOT MODIFY 2 | 3 | #include 4 | #include 5 | 6 | #ifndef __ANDROID__ 7 | #define NATIVE_FUNCTION(name) JNIEXPORT jlong JNICALL name(JNIEnv * _env, jclass _class) 8 | #else 9 | #define NATIVE_FUNCTION(name) JNIEXPORT jlong JNICALL name() 10 | #endif 11 | 12 | @FUNCTIONS@ 13 | -------------------------------------------------------------------------------- /ktreesitter-plugin/src/main/resources/jvm.kt.in: -------------------------------------------------------------------------------- 1 | // Automatically generated file. DO NOT MODIFY 2 | 3 | package @PACKAGE@ 4 | 5 | import java.io.File.createTempFile 6 | import javax.annotation.processing.Generated 7 | 8 | @Suppress("FunctionName") 9 | @Generated("io.github.treesitter.ktreesitter-plugin") 10 | actual object @CLASS@ { 11 | private const val LIB_NAME = "@LIBRARY@" 12 | 13 | init { 14 | try { 15 | System.loadLibrary(LIB_NAME) 16 | } catch (ex: UnsatisfiedLinkError) { 17 | @Suppress("UnsafeDynamicallyLoadedCode") 18 | System.load(libPath() ?: throw ex) 19 | } 20 | } 21 | 22 | @METHODS@ 23 | 24 | @JvmStatic 25 | @Suppress("ConvertToStringTemplate") 26 | @Throws(UnsupportedOperationException::class) 27 | internal fun libPath(): String? { 28 | val osName = System.getProperty("os.name")!!.lowercase() 29 | val archName = System.getProperty("os.arch")!!.lowercase() 30 | val ext: String 31 | val os: String 32 | val prefix: String 33 | when { 34 | "windows" in osName -> { 35 | ext = "dll" 36 | os = "windows" 37 | prefix = "" 38 | } 39 | "linux" in osName -> { 40 | ext = "so" 41 | os = "linux" 42 | prefix = "lib" 43 | } 44 | "mac" in osName -> { 45 | ext = "dylib" 46 | os = "macos" 47 | prefix = "lib" 48 | } 49 | else -> { 50 | throw UnsupportedOperationException("Unsupported operating system: " + osName) 51 | } 52 | } 53 | val arch = when { 54 | "amd64" in archName || "x86_64" in archName -> "x64" 55 | "aarch64" in archName || "arm64" in archName -> "aarch64" 56 | else -> throw UnsupportedOperationException("Unsupported architecture: " + archName) 57 | } 58 | val libPath = "/lib/" + os + "/" + arch + "/" + prefix + LIB_NAME + "." + ext 59 | val libUrl = javaClass.getResource(libPath) ?: return null 60 | return createTempFile(prefix + LIB_NAME, "." + ext).apply { 61 | writeBytes(libUrl.openStream().use { it.readAllBytes() }) 62 | deleteOnExit() 63 | }.path 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /ktreesitter-plugin/src/main/resources/native.kt.in: -------------------------------------------------------------------------------- 1 | // Automatically generated file. DO NOT MODIFY 2 | 3 | package @PACKAGE@ 4 | 5 | @IMPORTS@ 6 | import kotlinx.cinterop.ExperimentalForeignApi 7 | 8 | @OptIn(ExperimentalForeignApi::class) 9 | actual object @CLASS@ { 10 | @METHODS@ 11 | } 12 | -------------------------------------------------------------------------------- /ktreesitter/.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: LLVM 2 | AlignEscapedNewlinesLeft: false 3 | DerivePointerAlignment: false 4 | PointerAlignment: Right 5 | IndentWidth: 4 6 | ColumnLimit: 100 7 | IncludeBlocks: Preserve 8 | BinPackArguments: true 9 | IndentCaseLabels: true 10 | -------------------------------------------------------------------------------- /ktreesitter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.12.0) 2 | 3 | file(READ ../gradle.properties GRADLE_PROPERTIES) 4 | string(REGEX MATCH "project[.]version=([0-9.]+)" _ ${GRADLE_PROPERTIES}) 5 | 6 | project(ktreesitter VERSION ${CMAKE_MATCH_1} LANGUAGES C) 7 | 8 | find_package(JNI REQUIRED) 9 | 10 | set(CMAKE_C_STANDARD 11) 11 | 12 | if(MSVC) 13 | add_compile_options(/W3 /wd4244) 14 | else(MSVC) 15 | set(CMAKE_C_VISIBILITY_PRESET hidden) 16 | add_compile_options(-Wall -Wextra 17 | -Wno-unused-parameter 18 | -Wno-cast-function-type 19 | -Werror=incompatible-pointer-types 20 | -Werror=implicit-function-declaration) 21 | endif(MSVC) 22 | 23 | include_directories(${JNI_INCLUDE_DIRS} 24 | ../tree-sitter/lib/src 25 | ../tree-sitter/lib/include) 26 | 27 | add_compile_definitions(TREE_SITTER_HIDE_SYMBOLS) 28 | 29 | add_library(ktreesitter SHARED 30 | ./src/jni/language.c 31 | ./src/jni/lookahead_iterator.c 32 | ./src/jni/node.c 33 | ./src/jni/parser.c 34 | ./src/jni/query.c 35 | ./src/jni/tree.c 36 | ./src/jni/tree_cursor.c 37 | ./src/jni/module.c 38 | ../tree-sitter/lib/src/lib.c) 39 | 40 | set_target_properties(ktreesitter PROPERTIES DEFINE_SYMBOL "") 41 | 42 | install(TARGETS ktreesitter ARCHIVE EXCLUDE_FROM_ALL) 43 | -------------------------------------------------------------------------------- /ktreesitter/README.md: -------------------------------------------------------------------------------- 1 | # KTreeSitter 2 | 3 | Kotlin bindings to the [tree-sitter] parsing library. 4 | 5 | ## Supported platforms 6 | 7 | - [x] JVM 8 | - [x] Android 9 | - [x] Native 10 | - [ ] WASI 11 | 12 | *JS and WasmJS will not be supported.* 13 | 14 | ## Installation 15 | 16 | ```kotlin 17 | dependencies { 18 | implementation("io.github.tree-sitter:ktreesitter") version $ktreesitterVersion 19 | } 20 | 21 | repositories { 22 | mavenCentral() 23 | } 24 | ``` 25 | 26 | ## Basic usage 27 | 28 | ```kotlin 29 | val language = Language(TreeSitterKotlin.language()) 30 | val parser = Parser(language) 31 | val tree = parser.parse("fun main() {}") 32 | val rootNode = tree.rootNode 33 | 34 | assert(rootNode.type == "source_file") 35 | assert(rootNode.startPoint.column == 0) 36 | assert(rootNode.endPoint.column == 13) 37 | ``` 38 | 39 | [tree-sitter]: https://tree-sitter.github.io/tree-sitter/ 40 | -------------------------------------------------------------------------------- /ktreesitter/src/androidInstrumentedTest/kotlin/io/github/treesitter/ktreesitter/LanguageTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter 2 | 3 | import br.com.colman.kotest.KotestRunnerAndroid 4 | import io.github.treesitter.ktreesitter.java.TreeSitterJava 5 | import io.kotest.assertions.throwables.shouldNotThrowAny 6 | import io.kotest.core.spec.style.FunSpec 7 | import io.kotest.matchers.* 8 | import io.kotest.matchers.comparables.* 9 | import io.kotest.matchers.nulls.* 10 | import io.kotest.matchers.string.* 11 | import io.kotest.matchers.types.* 12 | import org.junit.runner.RunWith 13 | 14 | @RunWith(KotestRunnerAndroid::class) 15 | class LanguageTest : FunSpec({ 16 | val language = Language(TreeSitterJava.language()) 17 | 18 | test("version") { 19 | language.version shouldBe 14U 20 | } 21 | 22 | test("symbolCount") { 23 | language.symbolCount shouldBeGreaterThan 1U 24 | } 25 | 26 | test("stateCount") { 27 | language.stateCount shouldBeGreaterThan 1U 28 | } 29 | 30 | test("fieldCount") { 31 | language.fieldCount shouldBeGreaterThan 1U 32 | } 33 | 34 | test("symbolName()") { 35 | language.symbolName(1U) shouldBe "identifier" 36 | } 37 | 38 | test("symbolForName()") { 39 | language.symbolForName(";", false) shouldBeGreaterThan 0U 40 | language.symbolForName("program", true) shouldBeGreaterThan 0U 41 | } 42 | 43 | test("isNamed()") { 44 | language.isNamed(1U) shouldBe true 45 | } 46 | 47 | test("isVisible()") { 48 | language.isVisible(1U) shouldBe true 49 | } 50 | 51 | test("isSupertype()") { 52 | language.isSupertype(1U) shouldBe false 53 | } 54 | 55 | test("fieldNameForId()") { 56 | language.fieldNameForId(1U).shouldNotBeNull() 57 | } 58 | 59 | test("fieldIdForName()") { 60 | language.fieldIdForName("body") shouldBeGreaterThan 0U 61 | } 62 | 63 | test("nextState()") { 64 | val program = language.symbolForName("program", true) 65 | language.nextState(1U, program) shouldBeGreaterThan 0U 66 | } 67 | 68 | test("lookaheadIterator()") { 69 | val program = language.symbolForName("program", true) 70 | val state = language.nextState(1U, program) 71 | val lookahead = language.lookaheadIterator(state) 72 | lookahead.language shouldBe language 73 | } 74 | 75 | test("query()") { 76 | shouldNotThrowAny { language.query("(program) @root") } 77 | } 78 | 79 | test("equals()") { 80 | Language(TreeSitterJava.language()) shouldBe language.copy() 81 | } 82 | 83 | test("hashCode()") { 84 | language shouldHaveSameHashCodeAs TreeSitterJava.language() 85 | } 86 | 87 | test("toString()") { 88 | language.toString() shouldMatch Regex("""Language\(id=0x[0-9a-f]+, version=14\)""") 89 | } 90 | }) 91 | -------------------------------------------------------------------------------- /ktreesitter/src/androidInstrumentedTest/kotlin/io/github/treesitter/ktreesitter/LookaheadIteratorTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter 2 | 3 | import br.com.colman.kotest.KotestRunnerAndroid 4 | import io.github.treesitter.ktreesitter.java.TreeSitterJava 5 | import io.kotest.core.spec.style.FunSpec 6 | import io.kotest.matchers.* 7 | import io.kotest.matchers.collections.* 8 | import org.junit.runner.RunWith 9 | 10 | @RunWith(KotestRunnerAndroid::class) 11 | class LookaheadIteratorTest : FunSpec({ 12 | val language = Language(TreeSitterJava.language()) 13 | val state = language.nextState(1U, 138U) 14 | val lookahead = language.lookaheadIterator(state) 15 | 16 | test("currentSymbol") { 17 | lookahead.currentSymbol shouldBe UShort.MAX_VALUE 18 | lookahead.currentSymbolName shouldBe "ERROR" 19 | } 20 | 21 | test("next()") { 22 | lookahead.next() shouldBe LookaheadIterator.Symbol(0U, "end") 23 | } 24 | 25 | test("reset()") { 26 | lookahead.reset(state) shouldBe true 27 | lookahead.reset(state, language) shouldBe true 28 | } 29 | 30 | test("symbols()") { 31 | lookahead.symbols().shouldBeStrictlyIncreasing() 32 | } 33 | 34 | test("names()") { 35 | val names = lookahead.symbolNames().toList() 36 | names shouldContainExactly listOf("end", "line_comment", "block_comment") 37 | } 38 | 39 | test("iterator") { 40 | for ((symbol, name) in lookahead) { 41 | symbol shouldBe lookahead.currentSymbol 42 | name shouldBe lookahead.currentSymbolName 43 | } 44 | } 45 | }) 46 | -------------------------------------------------------------------------------- /ktreesitter/src/androidInstrumentedTest/kotlin/io/github/treesitter/ktreesitter/NodeTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter 2 | 3 | import br.com.colman.kotest.KotestRunnerAndroid 4 | import io.github.treesitter.ktreesitter.java.TreeSitterJava 5 | import io.kotest.assertions.throwables.shouldThrow 6 | import io.kotest.core.spec.style.FunSpec 7 | import io.kotest.inspectors.forSingle 8 | import io.kotest.matchers.* 9 | import io.kotest.matchers.collections.* 10 | import io.kotest.matchers.nulls.* 11 | import io.kotest.matchers.types.* 12 | import org.junit.runner.RunWith 13 | 14 | @RunWith(KotestRunnerAndroid::class) 15 | class NodeTest : FunSpec({ 16 | val language = Language(TreeSitterJava.language()) 17 | val parser = Parser(language) 18 | val source = "class Foo {}" 19 | val tree = parser.parse(source) 20 | val rootNode = tree.rootNode 21 | 22 | test("id") { 23 | rootNode.id shouldNotBe 0UL 24 | } 25 | 26 | test("symbol") { 27 | rootNode.symbol shouldNotBe UShort.MIN_VALUE 28 | } 29 | 30 | test("grammarSymbol") { 31 | rootNode.grammarSymbol shouldBe rootNode.symbol 32 | } 33 | 34 | test("type") { 35 | rootNode.type shouldBe "program" 36 | } 37 | 38 | test("grammarType") { 39 | rootNode.grammarType shouldBe rootNode.type 40 | } 41 | 42 | test("isNamed") { 43 | rootNode.isNamed shouldBe true 44 | } 45 | 46 | test("isExtra") { 47 | rootNode.isExtra shouldBe false 48 | } 49 | 50 | test("isError") { 51 | rootNode.isError shouldBe false 52 | } 53 | 54 | test("isMissing") { 55 | rootNode.isMissing shouldBe false 56 | } 57 | 58 | test("hasChanges") { 59 | rootNode.hasChanges shouldBe false 60 | } 61 | 62 | test("hasError") { 63 | rootNode.hasError shouldBe false 64 | } 65 | 66 | test("parseState") { 67 | rootNode.parseState shouldBe 0U 68 | } 69 | 70 | test("nextParseState") { 71 | rootNode.nextParseState shouldBe 0U 72 | } 73 | 74 | test("startByte") { 75 | rootNode.startByte shouldBe 0U 76 | } 77 | 78 | test("endByte") { 79 | rootNode.endByte shouldBe 12U 80 | } 81 | 82 | test("byteRange") { 83 | rootNode.byteRange shouldBe 0U..12U 84 | } 85 | 86 | test("range") { 87 | rootNode.range shouldBe Range(Point(0U, 0U), Point(0U, 12U), 0U, 12U) 88 | } 89 | 90 | test("startPoint") { 91 | rootNode.startPoint shouldBe Point(0U, 0U) 92 | } 93 | 94 | test("endPoint") { 95 | rootNode.endPoint shouldBe Point(0U, 12U) 96 | } 97 | 98 | test("childCount") { 99 | rootNode.childCount shouldBe 1U 100 | } 101 | 102 | test("namedChildCount") { 103 | rootNode.namedChildCount shouldBe 1U 104 | } 105 | 106 | test("descendantCount") { 107 | rootNode.descendantCount shouldBe 7U 108 | } 109 | 110 | test("parent") { 111 | rootNode.parent.shouldBeNull() 112 | } 113 | 114 | test("nextSibling") { 115 | rootNode.nextSibling.shouldBeNull() 116 | } 117 | 118 | test("prevSibling") { 119 | rootNode.prevSibling.shouldBeNull() 120 | } 121 | 122 | test("nextNamedSibling") { 123 | rootNode.nextNamedSibling.shouldBeNull() 124 | } 125 | 126 | test("prevNamedSibling") { 127 | rootNode.prevNamedSibling.shouldBeNull() 128 | } 129 | 130 | test("children") { 131 | val children = rootNode.children 132 | children.forSingle { it.type shouldBe "class_declaration" } 133 | rootNode.children shouldBeSameInstanceAs children 134 | } 135 | 136 | test("namedChildren") { 137 | val children = rootNode.namedChildren 138 | children shouldContainExactly rootNode.children 139 | } 140 | 141 | test("child()") { 142 | val node = rootNode.child(0U) 143 | node?.type shouldBe "class_declaration" 144 | shouldThrow { rootNode.child(1U) } 145 | } 146 | 147 | test("namedChild()") { 148 | rootNode.namedChild(0U) shouldBe rootNode.child(0U) 149 | shouldThrow { rootNode.namedChild(1U) } 150 | } 151 | 152 | test("childByFieldId()") { 153 | rootNode.childByFieldId(0U).shouldBeNull() 154 | } 155 | 156 | test("childByFieldName()") { 157 | val child = rootNode.child(0U)!!.childByFieldName("body") 158 | child?.type shouldBe "class_body" 159 | } 160 | 161 | test("childrenByFieldId()") { 162 | val id = language.fieldIdForName("name") 163 | val children = rootNode.child(0U)!!.childrenByFieldId(id) 164 | children.forSingle { it.type shouldBe "identifier" } 165 | } 166 | 167 | test("childrenByFieldName()") { 168 | val children = rootNode.child(0U)!!.childrenByFieldName("name") 169 | children.forSingle { it.type shouldBe "identifier" } 170 | } 171 | 172 | test("fieldNameForChild()") { 173 | rootNode.child(0U)!!.fieldNameForChild(2U) shouldBe "body" 174 | } 175 | 176 | test("fieldNameForNamedChild()") { 177 | rootNode.child(0U)!!.fieldNameForNamedChild(2U).shouldBeNull() 178 | } 179 | 180 | @Suppress("DEPRECATION") 181 | test("childContainingDescendant()") { 182 | val descendant = rootNode.child(0U)!!.child(0U)!! 183 | val child = rootNode.childContainingDescendant(descendant) 184 | child?.type shouldBe "class_declaration" 185 | } 186 | 187 | test("childWithDescendant()") { 188 | val descendant = rootNode.child(0U)!! 189 | val child = rootNode.childWithDescendant(descendant) 190 | child?.type shouldBe "class_declaration" 191 | } 192 | 193 | test("descendant()") { 194 | rootNode.descendant(0U, 5U)?.type shouldBe "class" 195 | rootNode.descendant(Point(0U, 10U), Point(0U, 12U))?.type shouldBe "class_body" 196 | } 197 | 198 | test("namedDescendant()") { 199 | rootNode.namedDescendant(0U, 5U)?.type shouldBe "class_declaration" 200 | rootNode.namedDescendant(Point(0U, 6U), Point(0U, 9U))?.type shouldBe "identifier" 201 | } 202 | 203 | test("walk()") { 204 | val cursor = rootNode.walk() 205 | cursor.currentNode shouldBeSameInstanceAs rootNode 206 | } 207 | 208 | test("copy()") { 209 | val copy = tree.copy() 210 | copy shouldNotBeSameInstanceAs tree 211 | copy.text() shouldBe tree.text() 212 | } 213 | 214 | @Suppress("UnnecessaryOptInAnnotation") 215 | @OptIn(ExperimentalMultiplatform::class) 216 | test("edit()") { 217 | val edit = InputEdit(0U, 12U, 10U, Point(0U, 0U), Point(0U, 12U), Point(0U, 10U)) 218 | val copy = tree.copy() 219 | val node = copy.rootNode 220 | copy.edit(edit) 221 | node.edit(edit) 222 | node.hasChanges shouldBe true 223 | } 224 | 225 | test("text()") { 226 | rootNode.descendant(6U, 9U)?.text() shouldBe "Foo" 227 | } 228 | 229 | test("sexp()") { 230 | rootNode.child(0U)!!.sexp() shouldBe 231 | "(class_declaration name: (identifier) body: (class_body))" 232 | } 233 | 234 | test("equals()") { 235 | rootNode shouldNotBe rootNode.child(0U) 236 | } 237 | 238 | test("hashCode()") { 239 | rootNode.hashCode() shouldBe rootNode.id.toInt() 240 | } 241 | 242 | test("toString()") { 243 | rootNode.toString() shouldBe "Node(type=program, startByte=0, endByte=12)" 244 | } 245 | }) 246 | -------------------------------------------------------------------------------- /ktreesitter/src/androidInstrumentedTest/kotlin/io/github/treesitter/ktreesitter/ParserTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter 2 | 3 | import br.com.colman.kotest.KotestRunnerAndroid 4 | import io.github.treesitter.ktreesitter.java.TreeSitterJava 5 | import io.kotest.assertions.throwables.shouldNotThrowAnyUnit 6 | import io.kotest.core.spec.style.FunSpec 7 | import io.kotest.inspectors.forSome 8 | import io.kotest.matchers.* 9 | import io.kotest.matchers.collections.* 10 | import io.kotest.matchers.nulls.* 11 | import io.kotest.matchers.string.* 12 | import io.kotest.matchers.types.* 13 | import org.junit.runner.RunWith 14 | 15 | @RunWith(KotestRunnerAndroid::class) 16 | class ParserTest : FunSpec({ 17 | val parser = Parser() 18 | 19 | test("language") { 20 | val language = Language(TreeSitterJava.language()) 21 | parser.language.shouldBeNull() 22 | parser.language = language 23 | parser.language shouldBeSameInstanceAs language 24 | } 25 | 26 | test("includedRanges") { 27 | val range = Range(Point(0U, 0U), Point(0U, 1U), 0U, 1U) 28 | parser.includedRanges.shouldBeEmpty() 29 | parser.includedRanges = listOf(range) 30 | parser.includedRanges shouldHaveSingleElement range 31 | } 32 | 33 | test("timeoutMicros") { 34 | parser.timeoutMicros shouldBe 0UL 35 | parser.timeoutMicros = 10000UL 36 | parser.timeoutMicros shouldBe 10000UL 37 | } 38 | 39 | test("logger") { 40 | shouldNotThrowAnyUnit { 41 | parser.logger = { _, _ -> 42 | throw UnsupportedOperationException() 43 | } 44 | } 45 | } 46 | 47 | test("parse(source)") { 48 | // UTF-8 49 | var source = "class Foo {}" 50 | var tree = parser.parse(source) 51 | tree.text()?.get(5) shouldBe ' ' 52 | 53 | // logging 54 | val logs = mutableListOf() 55 | parser.logger = { type, msg -> logs += "$type - $msg" } 56 | parser.reset() 57 | parser.parse(source) 58 | parser.logger = null 59 | logs.shouldNotBeEmpty().forSome { 60 | it shouldStartWith "LEX" 61 | } 62 | 63 | // UTF-16 64 | source = "var java = \"💩\"" 65 | tree = parser.parse(source) 66 | tree.text()?.subSequence(12, 14) shouldBe "\uD83D\uDCA9" 67 | } 68 | 69 | test("parse(callback)") { 70 | val source = "class Foo {}" 71 | val tree = parser.parse { byte, _ -> 72 | val end = minOf(byte.toInt() * 2, source.length) 73 | source.subSequence(byte.toInt(), end).ifEmpty { null } 74 | } 75 | tree.text().shouldBeNull() 76 | tree.rootNode.type shouldBe "program" 77 | } 78 | 79 | afterTest { (test, _) -> 80 | when (test.name.testName) { 81 | "includedRanges" -> parser.includedRanges = emptyList() 82 | "timeoutMicros" -> parser.timeoutMicros = 0UL 83 | "logger" -> parser.logger = null 84 | } 85 | } 86 | }) 87 | -------------------------------------------------------------------------------- /ktreesitter/src/androidInstrumentedTest/kotlin/io/github/treesitter/ktreesitter/TreeCursorTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter 2 | 3 | import br.com.colman.kotest.KotestRunnerAndroid 4 | import io.github.treesitter.ktreesitter.java.TreeSitterJava 5 | import io.kotest.core.spec.style.FunSpec 6 | import io.kotest.matchers.* 7 | import io.kotest.matchers.nulls.* 8 | import io.kotest.matchers.types.* 9 | import org.junit.runner.RunWith 10 | 11 | @RunWith(KotestRunnerAndroid::class) 12 | class TreeCursorTest : FunSpec({ 13 | val language = Language(TreeSitterJava.language()) 14 | val parser = Parser(language) 15 | val source = "class Foo {}" 16 | val tree = parser.parse(source) 17 | val cursor = tree.walk() 18 | val rootNode = tree.rootNode 19 | 20 | test("currentNode") { 21 | val node = cursor.currentNode 22 | node shouldBe rootNode 23 | node shouldBeSameInstanceAs cursor.currentNode 24 | } 25 | 26 | test("currentDepth") { 27 | cursor.currentDepth shouldBe 0U 28 | } 29 | 30 | test("currentFieldId") { 31 | cursor.currentFieldId shouldBe 0U 32 | } 33 | 34 | test("currentFieldName") { 35 | cursor.currentFieldName.shouldBeNull() 36 | } 37 | 38 | test("currentDescendantIndex") { 39 | cursor.currentDescendantIndex shouldBe 0U 40 | } 41 | 42 | test("copy()") { 43 | val copy = cursor.copy() 44 | copy shouldNotBeSameInstanceAs cursor 45 | cursor.reset(copy) 46 | copy.currentNode shouldBe cursor.currentNode 47 | } 48 | 49 | test("gotoFirstChild()") { 50 | cursor.gotoFirstChild() shouldBe true 51 | cursor.currentNode.type shouldBe "class_declaration" 52 | } 53 | 54 | test("gotoLastChild()") { 55 | cursor.gotoLastChild() shouldBe true 56 | cursor.currentFieldName shouldBe "body" 57 | } 58 | 59 | test("gotoParent()") { 60 | cursor.gotoParent() shouldBe true 61 | cursor.currentNode.type shouldBe "class_declaration" 62 | } 63 | 64 | test("gotoNextSibling()") { 65 | cursor.gotoNextSibling() shouldBe false 66 | } 67 | 68 | test("gotoPreviousSibling()") { 69 | cursor.gotoPreviousSibling() shouldBe false 70 | } 71 | 72 | test("gotoDescendant()") { 73 | cursor.gotoDescendant(2U) 74 | cursor.currentDescendantIndex shouldBe 2U 75 | cursor.reset(rootNode) 76 | } 77 | 78 | test("gotoFirstChildForByte()") { 79 | cursor.gotoFirstChildForByte(1U) shouldBe 0U 80 | cursor.currentNode.type shouldBe "class_declaration" 81 | } 82 | 83 | test("gotoFirstChildForPoint()") { 84 | cursor.gotoFirstChildForPoint(Point(0U, 7U)) shouldBe 1U 85 | cursor.currentFieldName shouldBe "name" 86 | } 87 | }) 88 | -------------------------------------------------------------------------------- /ktreesitter/src/androidInstrumentedTest/kotlin/io/github/treesitter/ktreesitter/TreeTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter 2 | 3 | import br.com.colman.kotest.KotestRunnerAndroid 4 | import io.github.treesitter.ktreesitter.java.TreeSitterJava 5 | import io.kotest.core.spec.style.FunSpec 6 | import io.kotest.matchers.* 7 | import io.kotest.matchers.collections.* 8 | import io.kotest.matchers.nulls.* 9 | import io.kotest.matchers.types.* 10 | import org.junit.runner.RunWith 11 | 12 | @RunWith(KotestRunnerAndroid::class) 13 | class TreeTest : FunSpec({ 14 | val language = Language(TreeSitterJava.language()) 15 | val parser = Parser(language) 16 | var source = "class Foo {}" 17 | var tree = parser.parse(source) 18 | 19 | test("language") { 20 | tree.language shouldBeSameInstanceAs language 21 | } 22 | 23 | test("rootNode") { 24 | tree.rootNode.endByte shouldBe source.length.toUInt() 25 | } 26 | 27 | test("includedRanges") { 28 | tree.includedRanges.shouldHaveSingleElement { 29 | it.startByte == 0U && it.endByte == UInt.MAX_VALUE 30 | } 31 | } 32 | 33 | test("rootNodeWithOffset()") { 34 | val node = tree.rootNodeWithOffset(6U, Point(0U, 6U)) 35 | node?.text() shouldBe "Foo {}" 36 | } 37 | 38 | test("text()") { 39 | tree.text() shouldBe source 40 | } 41 | 42 | test("edit()") { 43 | val edit = InputEdit(9U, 9U, 10U, Point(0U, 9U), Point(0U, 9U), Point(0U, 10U)) 44 | source = "class Foo2 {}" 45 | tree.edit(edit) 46 | tree.text().shouldBeNull() 47 | tree = parser.parse(source, tree) 48 | tree.text() shouldBe source 49 | } 50 | 51 | test("walk()") { 52 | val cursor = tree.walk() 53 | cursor.tree shouldBeSameInstanceAs tree 54 | } 55 | 56 | test("changedRanges()") { 57 | val edit = InputEdit(0U, 0U, 7U, Point(0U, 0U), Point(0U, 0U), Point(0U, 7U)) 58 | tree.edit(edit) 59 | val newTree = parser.parse("public $source", tree) 60 | tree.changedRanges(newTree).shouldHaveSingleElement { 61 | it.endByte == 7U && it.endPoint.column == 7U 62 | } 63 | } 64 | }) 65 | -------------------------------------------------------------------------------- /ktreesitter/src/androidInstrumentedTest/resources/kotest.properties: -------------------------------------------------------------------------------- 1 | # suppress inspection "UnusedProperty" for whole file 2 | kotest.framework.classpath.scanning.config.disable=true 3 | kotest.framework.classpath.scanning.autoscan.disable=true 4 | kotest.framework.assertion.globalassertsoftly=true 5 | -------------------------------------------------------------------------------- /ktreesitter/src/androidMain/kotlin/io/github/treesitter/ktreesitter/Language.kt: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter 2 | 3 | import dalvik.annotation.optimization.CriticalNative 4 | import dalvik.annotation.optimization.FastNative 5 | 6 | /** 7 | * A class that defines how to parse a particular language. 8 | * 9 | * When a [Language] is generated by the Tree-sitter CLI, it is assigned 10 | * an ABI [version] number that corresponds to the current CLI version. 11 | * 12 | * @constructor Create a new instance from the given language pointer. 13 | * @param language A pointer to a `TSLanguage` cast to [Long]. 14 | * @throws [IllegalArgumentException] If the pointer is invalid or the [version] is incompatible. 15 | */ 16 | actual class Language @Throws(IllegalArgumentException::class) actual constructor(language: Any) { 17 | @JvmField 18 | internal val self: Long = (language as? Long)?.takeIf { it > 0L } 19 | ?: throw IllegalArgumentException("Invalid language: $language") 20 | 21 | init { 22 | checkVersion() 23 | } 24 | 25 | /** The ABI version number for this language. */ 26 | @get:JvmName("getVersion") 27 | actual val version: UInt 28 | @FastNative external get 29 | 30 | /** The number of distinct node types in this language. */ 31 | @get:JvmName("getSymbolCount") 32 | actual val symbolCount: UInt 33 | @FastNative external get 34 | 35 | /** The number of valid states in this language. */ 36 | @get:JvmName("getStateCount") 37 | actual val stateCount: UInt 38 | @FastNative external get 39 | 40 | /** The number of distinct field names in this language. */ 41 | @get:JvmName("getFieldCount") 42 | actual val fieldCount: UInt 43 | @FastNative external get 44 | 45 | /** 46 | * Get another reference to the language. 47 | * 48 | * @since 0.24.0 49 | */ 50 | actual fun copy() = Language(copy(self)) 51 | 52 | /** Get the node type for the given numerical ID. */ 53 | @FastNative 54 | @JvmName("symbolName") 55 | actual external fun symbolName(symbol: UShort): String? 56 | 57 | /** Get the numerical ID for the given node type. */ 58 | @FastNative 59 | @JvmName("symbolForName") 60 | actual external fun symbolForName(name: String, isNamed: Boolean): UShort 61 | 62 | /** 63 | * Check if the node for the given numerical ID is named 64 | * 65 | * @see [Node.isNamed] 66 | */ 67 | @FastNative 68 | @JvmName("isNamed") 69 | actual external fun isNamed(symbol: UShort): Boolean 70 | 71 | /** Check if the node for the given numerical ID is visible. */ 72 | @FastNative 73 | @JvmName("isVisible") 74 | actual external fun isVisible(symbol: UShort): Boolean 75 | 76 | /** 77 | * Check if the node for the given numerical ID is a supertype. 78 | * 79 | * @since 0.24.0 80 | */ 81 | @FastNative 82 | @JvmName("isSupertype") 83 | actual external fun isSupertype(symbol: UShort): Boolean 84 | 85 | /** Get the field name for the given numerical id. */ 86 | @FastNative 87 | @JvmName("fieldNameForId") 88 | actual external fun fieldNameForId(id: UShort): String? 89 | 90 | /** Get the numerical ID for the given field name. */ 91 | @FastNative 92 | @JvmName("fieldIdForName") 93 | actual external fun fieldIdForName(name: String): UShort 94 | 95 | /** 96 | * Get the next parse state. 97 | * 98 | * Combine this with [lookaheadIterator] to generate 99 | * completion suggestions or valid symbols in error nodes. 100 | * 101 | * #### Example 102 | * 103 | * ```kotlin 104 | * language.nextState(node.parseState, node.grammarSymbol) 105 | * ``` 106 | */ 107 | @FastNative 108 | @JvmName("nextState") 109 | actual external fun nextState(state: UShort, symbol: UShort): UShort 110 | 111 | /** 112 | * Create a new [lookahead iterator][LookaheadIterator] for the given parse state. 113 | * 114 | * @throws [IllegalArgumentException] If the state is invalid for this language. 115 | */ 116 | @JvmName("lookaheadIterator") 117 | @Throws(IllegalArgumentException::class) 118 | actual fun lookaheadIterator(state: UShort) = LookaheadIterator(this, state) 119 | 120 | /** 121 | * Create a new [Query] from a string containing one or more S-expression 122 | * [patterns](https://tree-sitter.github.io/tree-sitter/using-parsers#query-syntax). 123 | * 124 | * @throws [QueryError] If any error occurred while creating the query. 125 | */ 126 | @Throws(QueryError::class) 127 | actual fun query(source: String) = Query(this, source) 128 | 129 | actual override fun equals(other: Any?) = 130 | this === other || (other is Language && self == other.self) 131 | 132 | actual override fun hashCode() = self.hashCode() 133 | 134 | override fun toString() = "Language(id=0x${self.toString(16)}, version=$version)" 135 | 136 | @FastNative 137 | @Throws(IllegalArgumentException::class) 138 | private external fun checkVersion() 139 | 140 | private companion object { 141 | @JvmStatic 142 | @CriticalNative 143 | private external fun copy(self: Long): Long 144 | 145 | init { 146 | System.loadLibrary("ktreesitter") 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /ktreesitter/src/androidMain/kotlin/io/github/treesitter/ktreesitter/LookaheadIterator.kt: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter 2 | 3 | import dalvik.annotation.optimization.CriticalNative 4 | import dalvik.annotation.optimization.FastNative 5 | 6 | /** 7 | * A class that is used to look up valid symbols in a specific parse state. 8 | * 9 | * Lookahead iterators can be useful to generate suggestions and improve syntax 10 | * error diagnostics. To get symbols valid in an `ERROR` node, use the lookahead 11 | * iterator on its first leaf node state. For `MISSING` nodes, a lookahead 12 | * iterator created on the previous non-extra leaf node may be appropriate. 13 | * 14 | * __NOTE:__ If you're targeting Android SDK level < 33, 15 | * you must `use` or [close] the instance to free up resources. 16 | */ 17 | actual class LookaheadIterator @Throws(IllegalArgumentException::class) internal constructor( 18 | language: Language, 19 | private val state: UShort 20 | ) : AbstractIterator(), AutoCloseable { 21 | private val self: Long = init(language.self, state).takeIf { it > 0L } 22 | ?: throw IllegalArgumentException("State $state is not valid for $language") 23 | 24 | init { 25 | RefCleaner(this, CleanAction(self)) 26 | } 27 | 28 | /** The current language of the lookahead iterator. */ 29 | actual val language: Language 30 | @FastNative external get 31 | 32 | /** 33 | * The current symbol ID. 34 | * 35 | * The ID of the `ERROR` symbol is equal to `UShort.MAX_VALUE`. 36 | */ 37 | @get:JvmName("getCurrentSymbol") 38 | actual val currentSymbol: UShort 39 | @FastNative external get 40 | 41 | /** 42 | * The current symbol name. 43 | * 44 | * Newly created lookahead iterators will contain the `ERROR` symbol. 45 | */ 46 | actual val currentSymbolName: String 47 | @FastNative external get 48 | 49 | /** 50 | * Reset the lookahead iterator the given [state] and, optionally, another [language]. 51 | * 52 | * @return `true` if the iterator was reset successfully or `false` if it failed. 53 | */ 54 | @FastNative 55 | @JvmName("reset") 56 | actual external fun reset(state: UShort, language: Language?): Boolean 57 | 58 | /** Advance the lookahead iterator to the next symbol. */ 59 | actual override fun next() = super.next() 60 | 61 | /** Iterate over the symbol IDs. */ 62 | actual fun symbols(): Sequence { 63 | reset(state) 64 | return sequence { 65 | while (nativeNext()) { 66 | yield(currentSymbol) 67 | } 68 | } 69 | } 70 | 71 | /** Iterate over the symbol names. */ 72 | actual fun symbolNames(): Sequence { 73 | reset(state) 74 | return sequence { 75 | while (nativeNext()) { 76 | yield(currentSymbolName) 77 | } 78 | } 79 | } 80 | 81 | override fun close() = delete(self) 82 | 83 | override fun computeNext() = if (nativeNext()) { 84 | setNext(Symbol(currentSymbol, currentSymbolName)) 85 | } else { 86 | done() 87 | } 88 | 89 | operator fun iterator() = apply { reset(state) } 90 | 91 | @FastNative 92 | private external fun nativeNext(): Boolean 93 | 94 | /** A class that pairs a symbol ID with its name. */ 95 | @JvmRecord 96 | actual data class Symbol actual constructor(actual val id: UShort, actual val name: String) 97 | 98 | private class CleanAction(private val ptr: Long) : Runnable { 99 | override fun run() = delete(ptr) 100 | } 101 | 102 | private companion object { 103 | @JvmStatic 104 | @JvmName("init") 105 | @CriticalNative 106 | private external fun init(language: Long, state: UShort): Long 107 | 108 | @JvmStatic 109 | @CriticalNative 110 | private external fun delete(self: Long) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /ktreesitter/src/androidMain/kotlin/io/github/treesitter/ktreesitter/Parser.kt: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter 2 | 3 | import dalvik.annotation.optimization.CriticalNative 4 | import dalvik.annotation.optimization.FastNative 5 | 6 | /** 7 | * A class that is used to produce a [syntax tree][Tree] from source code. 8 | * 9 | * __NOTE:__ If you're targeting Android SDK level < 33, 10 | * you must `use` or [close] the instance to free up resources. 11 | * 12 | * @constructor Create a new instance with a certain [language], or `null` if empty. 13 | */ 14 | actual class Parser actual constructor() : AutoCloseable { 15 | actual constructor(language: Language) : this() { 16 | this.language = language 17 | } 18 | 19 | private val self = init() 20 | 21 | init { 22 | RefCleaner(this, CleanAction(self)) 23 | } 24 | 25 | /** 26 | * The language that the parser will use for parsing. 27 | * 28 | * Parsing cannot be performed while the language is `null`. 29 | */ 30 | actual var language: Language? = null 31 | @FastNative external set 32 | 33 | /** 34 | * The ranges of text that the parser will include when parsing. 35 | * 36 | * By default, the parser will always include entire documents. 37 | * Setting this property allows you to parse only a _portion_ of a 38 | * document but still return a syntax tree whose ranges match up with 39 | * the document as a whole. You can also pass multiple disjoint ranges. 40 | * 41 | * @throws [IllegalArgumentException] If the ranges overlap or are not in ascending order. 42 | */ 43 | @set:Throws(IllegalArgumentException::class) 44 | actual var includedRanges: List = emptyList() 45 | external set 46 | 47 | /** 48 | * The maximum duration in microseconds that parsing 49 | * should be allowed to take before halting. 50 | */ 51 | @get:JvmName("getTimeoutMicros") 52 | @set:JvmName("setTimeoutMicros") 53 | actual var timeoutMicros: ULong 54 | @FastNative external get 55 | 56 | @FastNative external set 57 | 58 | /** 59 | * The logger that the parser will use during parsing. 60 | * 61 | * #### Example 62 | * 63 | * ``` 64 | * import android.util.Log; 65 | * 66 | * parser.logger = { type, msg -> 67 | * Log.d("TS ${type.name}", msg) 68 | * } 69 | * ``` 70 | */ 71 | @get:Deprecated("The logger can't be called directly.", level = DeprecationLevel.HIDDEN) 72 | actual var logger: LogFunction? = null 73 | @FastNative external set 74 | 75 | /** 76 | * Parse a source code string and create a syntax tree. 77 | * 78 | * If you have already parsed an earlier version of this document and the document 79 | * has since been edited, pass the previous syntax tree to [oldTree] so that the 80 | * unchanged parts of it can be reused. This will save time and memory. For this 81 | * to work correctly, you must have already edited the old syntax tree using the 82 | * [Tree.edit] method in a way that exactly matches the source code changes. 83 | * 84 | * @throws [IllegalStateException] 85 | * If the parser does not have a [language] assigned or 86 | * if parsing was cancelled due to a [timeout][timeoutMicros]. 87 | */ 88 | @Throws(IllegalStateException::class) 89 | actual external fun parse(source: String, oldTree: Tree?): Tree 90 | 91 | /** 92 | * Parse source code from a callback and create a syntax tree. 93 | * 94 | * If you have already parsed an earlier version of this document and the document 95 | * has since been edited, pass the previous syntax tree to [oldTree] so that the 96 | * unchanged parts of it can be reused. This will save time and memory. For this 97 | * to work correctly, you must have already edited the old syntax tree using the 98 | * [Tree.edit] method in a way that exactly matches the source code changes. 99 | * 100 | * @throws [IllegalStateException] 101 | * If the parser does not have a [language] assigned or 102 | * if parsing was cancelled due to a [timeout][timeoutMicros]. 103 | */ 104 | @Throws(IllegalStateException::class) 105 | actual external fun parse(oldTree: Tree?, callback: ParseCallback): Tree 106 | 107 | /** 108 | * Instruct the parser to start the next [parse] from the beginning. 109 | * 110 | * If the parser previously failed because of a [timeout][timeoutMicros], 111 | * then by default, it will resume where it left off. If you don't 112 | * want to resume, and instead intend to use this parser to parse 113 | * some other document, you must call this method first. 114 | */ 115 | @FastNative 116 | actual external fun reset() 117 | 118 | override fun toString() = "Parser(language=$language)" 119 | 120 | override fun close() = delete(self) 121 | 122 | /** The type of a log message. */ 123 | @Suppress("unused") 124 | actual enum class LogType { LEX, PARSE } 125 | 126 | private class CleanAction(private val ptr: Long) : Runnable { 127 | override fun run() = delete(ptr) 128 | } 129 | 130 | private companion object { 131 | @JvmStatic 132 | @CriticalNative 133 | private external fun init(): Long 134 | 135 | @JvmStatic 136 | @FastNative 137 | private external fun delete(self: Long) 138 | 139 | init { 140 | System.loadLibrary("ktreesitter") 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /ktreesitter/src/androidMain/kotlin/io/github/treesitter/ktreesitter/RefCleaner.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("RefCleaner") 2 | 3 | package io.github.treesitter.ktreesitter 4 | 5 | import android.os.Build.VERSION.SDK_INT 6 | import android.os.Build.VERSION_CODES.TIRAMISU 7 | import java.lang.ref.Cleaner 8 | 9 | internal object RefCleaner { 10 | private val INSTANCE = if (SDK_INT < TIRAMISU) null else Cleaner.create() 11 | 12 | @JvmName("register") 13 | operator fun invoke(obj: Any, action: Runnable) { 14 | if (SDK_INT >= TIRAMISU) INSTANCE!!.register(obj, action) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ktreesitter/src/androidMain/kotlin/io/github/treesitter/ktreesitter/Tree.kt: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter 2 | 3 | import dalvik.annotation.optimization.CriticalNative 4 | import dalvik.annotation.optimization.FastNative 5 | 6 | /** 7 | * A class that represents a syntax tree. 8 | * 9 | * __NOTE:__ If you're targeting Android SDK level < 33, 10 | * you must `use` or [close] the instance to free up resources. 11 | */ 12 | actual class Tree internal constructor( 13 | private val self: Long, 14 | private var source: String?, 15 | /** The language that was used to parse the syntax tree. */ 16 | actual val language: Language 17 | ) : AutoCloseable { 18 | init { 19 | RefCleaner(this, CleanAction(self)) 20 | } 21 | 22 | /** The root node of the syntax tree. */ 23 | actual val rootNode: Node 24 | @FastNative external get 25 | 26 | /** The included ranges that were used to parse the syntax tree. */ 27 | actual val includedRanges by lazy { nativeIncludedRanges() } 28 | 29 | /** 30 | * Get the root node of the syntax tree, but with 31 | * its position shifted forward by the given offset. 32 | */ 33 | @FastNative 34 | @JvmName("rootNodeWithOffset") 35 | actual external fun rootNodeWithOffset(bytes: UInt, extent: Point): Node? 36 | 37 | /** 38 | * Edit the syntax tree to keep it in sync 39 | * with source code that has been modified. 40 | */ 41 | @FastNative 42 | actual external fun edit(edit: InputEdit) 43 | 44 | /** 45 | * Create a shallow copy of the syntax tree. 46 | * 47 | * You need to copy a syntax tree in order to use it on multiple 48 | * threads or coroutines, as syntax trees are not thread safe. 49 | */ 50 | actual fun copy() = Tree(copy(self), source, language) 51 | 52 | /** Create a new tree cursor starting from the node of the tree. */ 53 | actual fun walk() = TreeCursor(rootNode) 54 | 55 | /** Get the source code of the syntax tree, if available. */ 56 | actual fun text(): CharSequence? = source 57 | 58 | /** 59 | * Compare an old edited syntax tree to a new 60 | * syntax tree representing the same document. 61 | * 62 | * For this to work correctly, this tree must have been 63 | * edited such that its ranges match up to the new tree. 64 | * 65 | * @return A list of ranges whose syntactic structure has changed. 66 | */ 67 | actual external fun changedRanges(newTree: Tree): List 68 | 69 | override fun toString() = "Tree(language=$language, source=$source)" 70 | 71 | override fun close() = delete(self) 72 | 73 | private external fun nativeIncludedRanges(): List 74 | 75 | private class CleanAction(private val ptr: Long) : Runnable { 76 | override fun run() = delete(ptr) 77 | } 78 | 79 | private companion object { 80 | @JvmStatic 81 | @CriticalNative 82 | private external fun copy(self: Long): Long 83 | 84 | @JvmStatic 85 | @CriticalNative 86 | private external fun delete(self: Long) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /ktreesitter/src/androidMain/kotlin/io/github/treesitter/ktreesitter/TreeCursor.kt: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter 2 | 3 | import dalvik.annotation.optimization.CriticalNative 4 | import dalvik.annotation.optimization.FastNative 5 | 6 | /** 7 | * A class that can be used to efficiently walk a [syntax tree][Tree]. 8 | * 9 | * __NOTE:__ If you're targeting Android SDK level < 33, 10 | * you must `use` or [close] the instance to free up resources. 11 | */ 12 | actual class TreeCursor private constructor( 13 | private val self: Long, 14 | @JvmField internal actual val tree: Tree 15 | ) : AutoCloseable { 16 | internal constructor(node: Node) : this(init(node), node.tree) { 17 | internalNode = node 18 | } 19 | 20 | init { 21 | RefCleaner(this, CleanAction(self)) 22 | } 23 | 24 | @Suppress("unused") 25 | private var internalNode: Node? = null 26 | 27 | /** The current node of the cursor. */ 28 | actual val currentNode: Node 29 | @FastNative external get 30 | 31 | /** 32 | * The depth of the cursor's current node relative to the 33 | * original node that the cursor was constructed with. 34 | */ 35 | @get:JvmName("getCurrentDepth") 36 | actual val currentDepth: UInt 37 | @FastNative external get 38 | 39 | /** 40 | * The field ID of the tree cursor's current node, or `0`. 41 | * 42 | * @see [Node.childByFieldId] 43 | * @see [Language.fieldIdForName] 44 | */ 45 | @get:JvmName("getCurrentFieldId") 46 | actual val currentFieldId: UShort 47 | @FastNative external get 48 | 49 | /** 50 | * The field name of the tree cursor's current node, if available. 51 | * 52 | * @see [Node.childByFieldName] 53 | */ 54 | actual val currentFieldName: String? 55 | @FastNative external get 56 | 57 | /** 58 | * The index of the cursor's current node out of all the descendants 59 | * of the original node that the cursor was constructed with. 60 | */ 61 | @get:JvmName("getCurrentDescendantIndex") 62 | actual val currentDescendantIndex: UInt 63 | @FastNative external get 64 | 65 | /** Create a shallow copy of the tree cursor. */ 66 | actual fun copy() = TreeCursor(copy(self), tree) 67 | 68 | /** Reset the cursor to start at a different node. */ 69 | @FastNative 70 | actual external fun reset(node: Node) 71 | 72 | /** Reset the cursor to start at the same position as another cursor. */ 73 | @FastNative 74 | actual external fun reset(cursor: TreeCursor) 75 | 76 | /** 77 | * Move the cursor to the first child of its current node. 78 | * 79 | * @return 80 | * `true` if the cursor successfully moved, 81 | * or `false` if there were no children. 82 | */ 83 | @FastNative 84 | actual external fun gotoFirstChild(): Boolean 85 | 86 | /** 87 | * Move the cursor to the last child of its current node. 88 | * 89 | * @return 90 | * `true` if the cursor successfully moved, 91 | * or `false` if there were no children. 92 | */ 93 | @FastNative 94 | actual external fun gotoLastChild(): Boolean 95 | 96 | /** 97 | * Move the cursor to the parent of its current node. 98 | * 99 | * @return 100 | * `true` if the cursor successfully moved, 101 | * or `false` if there was no parent node. 102 | */ 103 | @FastNative 104 | actual external fun gotoParent(): Boolean 105 | 106 | /** 107 | * Move the cursor to the next sibling of its current node. 108 | * 109 | * @return 110 | * `true` if the cursor successfully moved, 111 | * or `false` if there was no next sibling node. 112 | */ 113 | @FastNative 114 | actual external fun gotoNextSibling(): Boolean 115 | 116 | /** 117 | * Move the cursor to the previous sibling of its current node. 118 | * 119 | * This function may be slower than [gotoNextSibling] due to how node positions 120 | * are stored. In the worst case, this will need to iterate through all the 121 | * children up to the previous sibling node to recalculate its position. 122 | * 123 | * @return 124 | * `true` if the cursor successfully moved, 125 | * or `false` if there was no previous sibling node. 126 | */ 127 | actual external fun gotoPreviousSibling(): Boolean 128 | 129 | /** 130 | * Move the cursor to the node that is the nth descendant of 131 | * the original node that the cursor was constructed with, 132 | * where `0` represents the original node itself. 133 | */ 134 | @FastNative 135 | @JvmName("gotoDescendant") 136 | actual external fun gotoDescendant(index: UInt) 137 | 138 | /** 139 | * Move the cursor to the first child of its current 140 | * node that extends beyond the given byte offset. 141 | * 142 | * @return The index of the child node, or `null` if no such child was found. 143 | */ 144 | @JvmName("gotoFirstChildForByte") 145 | actual fun gotoFirstChildForByte(byte: UInt): UInt? { 146 | val result = nativeGotoFirstChildForByte(byte) 147 | if (result == -1L) return null 148 | internalNode = null 149 | return result.toUInt() 150 | } 151 | 152 | /** 153 | * Move the cursor to the first child of its current 154 | * node that extends beyond the given point offset. 155 | * 156 | * @return The index of the child node, or `null` if no such child was found. 157 | */ 158 | @JvmName("gotoFirstChildForPoint") 159 | actual fun gotoFirstChildForPoint(point: Point): UInt? { 160 | val result = nativeGotoFirstChildForPoint(point) 161 | if (result == -1L) return null 162 | internalNode = null 163 | return result.toUInt() 164 | } 165 | 166 | override fun toString() = "TreeCursor(tree=$tree)" 167 | 168 | override fun close() = delete(self) 169 | 170 | @FastNative 171 | @JvmName("nativeGotoFirstChildForByte") 172 | private external fun nativeGotoFirstChildForByte(byte: UInt): Long 173 | 174 | @FastNative 175 | @JvmName("nativeGotoFirstChildForPoint") 176 | private external fun nativeGotoFirstChildForPoint(point: Point): Long 177 | 178 | private class CleanAction(private val ptr: Long) : Runnable { 179 | override fun run() = delete(ptr) 180 | } 181 | 182 | private companion object { 183 | @JvmStatic 184 | @FastNative 185 | private external fun init(node: Node): Long 186 | 187 | @JvmStatic 188 | @CriticalNative 189 | private external fun copy(self: Long): Long 190 | 191 | @JvmStatic 192 | @CriticalNative 193 | private external fun delete(self: Long) 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /ktreesitter/src/commonMain/kotlin/io/github/treesitter/ktreesitter/InputEdit.kt: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter 2 | 3 | import kotlin.jvm.JvmName 4 | 5 | /** An edit to a text document. */ 6 | data class InputEdit( 7 | @get:JvmName("startByte") val startByte: UInt, 8 | @get:JvmName("oldEndByte") val oldEndByte: UInt, 9 | @get:JvmName("newEndByte") val newEndByte: UInt, 10 | @get:JvmName("startPoint") val startPoint: Point, 11 | @get:JvmName("oldEndPoint") val oldEndPoint: Point, 12 | @get:JvmName("newEndPoint") val newEndPoint: Point 13 | ) 14 | -------------------------------------------------------------------------------- /ktreesitter/src/commonMain/kotlin/io/github/treesitter/ktreesitter/Language.kt: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter 2 | 3 | /** 4 | * A class that defines how to parse a particular language. 5 | * 6 | * When a [Language] is generated by the Tree-sitter CLI, it is assigned 7 | * an ABI [version] number that corresponds to the current CLI version. 8 | * 9 | * @constructor Create a new instance from the given language pointer. 10 | * @throws [IllegalArgumentException] If the pointer is invalid or the [version] is incompatible. 11 | */ 12 | expect class Language @Throws(IllegalArgumentException::class) constructor(language: Any) { 13 | /** The ABI version number for this language. */ 14 | val version: UInt 15 | 16 | /** The number of distinct node types in this language. */ 17 | val symbolCount: UInt 18 | 19 | /** The number of valid states in this language. */ 20 | val stateCount: UInt 21 | 22 | /** The number of distinct field names in this language. */ 23 | val fieldCount: UInt 24 | 25 | /** 26 | * Get another reference to the language. 27 | * 28 | * @since 0.24.0 29 | */ 30 | fun copy(): Language 31 | 32 | /** Get the node type for the given numerical ID. */ 33 | fun symbolName(symbol: UShort): String? 34 | 35 | /** Get the numerical ID for the given node type. */ 36 | fun symbolForName(name: String, isNamed: Boolean): UShort 37 | 38 | /** 39 | * Check if the node for the given numerical ID is named 40 | * 41 | * @see [Node.isNamed] 42 | */ 43 | fun isNamed(symbol: UShort): Boolean 44 | 45 | /** Check if the node for the given numerical ID is visible. */ 46 | fun isVisible(symbol: UShort): Boolean 47 | 48 | /** 49 | * Check if the node for the given numerical ID is a supertype. 50 | * 51 | * @since 0.24.0 52 | */ 53 | fun isSupertype(symbol: UShort): Boolean 54 | 55 | /** Get the field name for the given numerical id. */ 56 | fun fieldNameForId(id: UShort): String? 57 | 58 | /** Get the numerical ID for the given field name. */ 59 | fun fieldIdForName(name: String): UShort 60 | 61 | /** 62 | * Get the next parse state. 63 | * 64 | * Combine this with [lookaheadIterator] to generate 65 | * completion suggestions or valid symbols in error nodes. 66 | * 67 | * #### Example 68 | * 69 | * ```kotlin 70 | * language.nextState(node.parseState, node.grammarSymbol) 71 | * ``` 72 | */ 73 | fun nextState(state: UShort, symbol: UShort): UShort 74 | 75 | /** 76 | * Create a new [lookahead iterator][LookaheadIterator] for the given parse state. 77 | * 78 | * @throws [IllegalArgumentException] If the state is invalid for this language. 79 | */ 80 | @Throws(IllegalArgumentException::class) 81 | fun lookaheadIterator(state: UShort): LookaheadIterator 82 | 83 | /** 84 | * Create a new [Query] from a string containing one or more S-expression 85 | * [patterns](https://tree-sitter.github.io/tree-sitter/using-parsers#query-syntax). 86 | * 87 | * @throws [QueryError] If any error occurred while creating the query. 88 | */ 89 | @Throws(QueryError::class) 90 | fun query(source: String): Query 91 | 92 | override fun equals(other: Any?): Boolean 93 | 94 | override fun hashCode(): Int 95 | } 96 | -------------------------------------------------------------------------------- /ktreesitter/src/commonMain/kotlin/io/github/treesitter/ktreesitter/LookaheadIterator.kt: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter 2 | 3 | /** 4 | * A class that is used to look up valid symbols in a specific parse state. 5 | * 6 | * Lookahead iterators can be useful to generate suggestions and improve syntax 7 | * error diagnostics. To get symbols valid in an `ERROR` node, use the lookahead 8 | * iterator on its first leaf node state. For `MISSING` nodes, a lookahead 9 | * iterator created on the previous non-extra leaf node may be appropriate. 10 | */ 11 | expect class LookaheadIterator : AbstractIterator { 12 | /** The current language of the lookahead iterator. */ 13 | val language: Language 14 | 15 | /** 16 | * The current symbol ID. 17 | * 18 | * The ID of the `ERROR` symbol is equal to `UShort.MAX_VALUE`. 19 | */ 20 | val currentSymbol: UShort 21 | 22 | /** 23 | * The current symbol name. 24 | * 25 | * Newly created lookahead iterators will contain the `ERROR` symbol. 26 | */ 27 | val currentSymbolName: String 28 | 29 | /** 30 | * Reset the lookahead iterator the given [state] and, optionally, another [language]. 31 | * 32 | * @return `true` if the iterator was reset successfully or `false` if it failed. 33 | */ 34 | fun reset(state: UShort, language: Language? = null): Boolean 35 | 36 | /** Advance the lookahead iterator to the next symbol. */ 37 | override fun next(): Symbol 38 | 39 | /** Iterate over the symbol IDs. */ 40 | fun symbols(): Sequence 41 | 42 | /** Iterate over the symbol names. */ 43 | fun symbolNames(): Sequence 44 | 45 | /** A class that pairs a symbol ID with its name. */ 46 | class Symbol(id: UShort, name: String) { 47 | val id: UShort 48 | val name: String 49 | 50 | operator fun component1(): UShort 51 | operator fun component2(): String 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /ktreesitter/src/commonMain/kotlin/io/github/treesitter/ktreesitter/Parser.kt: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter 2 | 3 | /** 4 | * A function to retrieve a chunk of text at a given byte offset and point. 5 | * 6 | * The function should return `null` to indicate the end of the document. 7 | */ 8 | typealias ParseCallback = (byte: UInt, point: Point) -> CharSequence? 9 | 10 | /** 11 | * A function that logs parsing results. 12 | * 13 | * The first argument is the log type and the second argument is the message. 14 | */ 15 | typealias LogFunction = (type: Parser.LogType, message: String) -> Unit 16 | 17 | /** 18 | * A class that is used to produce a [syntax tree][Tree] from source code. 19 | * 20 | * @constructor Create a new instance with a certain [language], or `null` if empty. 21 | */ 22 | expect class Parser() { 23 | constructor(language: Language) 24 | 25 | /** 26 | * The language that the parser will use for parsing. 27 | * 28 | * Parsing cannot be performed while the language is `null`. 29 | */ 30 | var language: Language? 31 | 32 | /** 33 | * The ranges of text that the parser will include when parsing. 34 | * 35 | * By default, the parser will always include entire documents. 36 | * Setting this property allows you to parse only a _portion_ of a 37 | * document but still return a syntax tree whose ranges match up with 38 | * the document as a whole. You can also pass multiple disjoint ranges. 39 | * 40 | * @throws [IllegalArgumentException] If the ranges overlap or are not in ascending order. 41 | */ 42 | var includedRanges: List 43 | 44 | /** 45 | * The maximum duration in microseconds that parsing 46 | * should be allowed to take before halting. 47 | */ 48 | var timeoutMicros: ULong 49 | 50 | /** The logger that the parser will use during parsing. */ 51 | @get:Deprecated("The logger can't be called directly.", level = DeprecationLevel.HIDDEN) 52 | var logger: LogFunction? 53 | 54 | // TODO: add cancellationFlag 55 | 56 | /** 57 | * Parse a source code string and create a syntax tree. 58 | * 59 | * If you have already parsed an earlier version of this document and the document 60 | * has since been edited, pass the previous syntax tree to [oldTree] so that the 61 | * unchanged parts of it can be reused. This will save time and memory. For this 62 | * to work correctly, you must have already edited the old syntax tree using the 63 | * [Tree.edit] method in a way that exactly matches the source code changes. 64 | * 65 | * @throws [IllegalStateException] 66 | * If the parser does not have a [language] assigned or 67 | * if parsing was cancelled due to a [timeout][timeoutMicros]. 68 | */ 69 | @Throws(IllegalStateException::class) 70 | fun parse(source: String, oldTree: Tree? = null): Tree 71 | 72 | /** 73 | * Parse source code from a callback and create a syntax tree. 74 | * 75 | * If you have already parsed an earlier version of this document and the document 76 | * has since been edited, pass the previous syntax tree to [oldTree] so that the 77 | * unchanged parts of it can be reused. This will save time and memory. For this 78 | * to work correctly, you must have already edited the old syntax tree using the 79 | * [Tree.edit] method in a way that exactly matches the source code changes. 80 | * 81 | * @throws [IllegalStateException] 82 | * If the parser does not have a [language] assigned or 83 | * if parsing was cancelled due to a [timeout][timeoutMicros]. 84 | */ 85 | @Throws(IllegalStateException::class) 86 | fun parse(oldTree: Tree? = null, callback: ParseCallback): Tree 87 | 88 | /** 89 | * Instruct the parser to start the next [parse] from the beginning. 90 | * 91 | * If the parser previously failed because of a [timeout][timeoutMicros], 92 | * then by default, it will resume where it left off. If you don't 93 | * want to resume, and instead intend to use this parser to parse 94 | * some other document, you must call this method first. 95 | */ 96 | fun reset() 97 | 98 | /** The type of a log message. */ 99 | enum class LogType { LEX, PARSE } 100 | } 101 | -------------------------------------------------------------------------------- /ktreesitter/src/commonMain/kotlin/io/github/treesitter/ktreesitter/Point.kt: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter 2 | 3 | import kotlin.jvm.JvmField 4 | import kotlin.jvm.JvmName 5 | 6 | /** 7 | * A position in a text document in terms of rows and columns. 8 | * 9 | * @property row The zero-based row of the document. 10 | * @property column The zero-based column of the document. 11 | */ 12 | data class Point( 13 | @get:JvmName("row") val row: UInt, 14 | @get:JvmName("column") val column: UInt 15 | ) : Comparable { 16 | override operator fun compareTo(other: Point): Int { 17 | val rowDiff = row.compareTo(other.row) 18 | if (rowDiff != 0) return rowDiff 19 | return column.compareTo(other.column) 20 | } 21 | 22 | companion object { 23 | /** The minimum value a [Point] can have. */ 24 | @JvmField 25 | val MIN = Point(UInt.MIN_VALUE, UInt.MIN_VALUE) 26 | 27 | /** The maximum value a [Point] can have. */ 28 | @JvmField 29 | val MAX = Point(UInt.MAX_VALUE, UInt.MAX_VALUE) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /ktreesitter/src/commonMain/kotlin/io/github/treesitter/ktreesitter/Query.kt: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter 2 | 3 | /** 4 | * A class that represents a set of patterns which match nodes in a syntax tree. 5 | * 6 | * @constructor 7 | * Create a new query from a particular language and a 8 | * string containing one or more S-expression patterns. 9 | * @throws [QueryError] If any error occurred while creating the query. 10 | */ 11 | expect class Query @Throws(QueryError::class) constructor(language: Language, source: String) { 12 | /** The number of patterns in the query. */ 13 | val patternCount: UInt 14 | 15 | /** The number of captures in the query. */ 16 | val captureCount: UInt 17 | 18 | /** 19 | * The maximum duration in microseconds that query 20 | * execution should be allowed to take before halting. 21 | * 22 | * Default: `0` 23 | * 24 | * @since 0.23.0 25 | */ 26 | var timeoutMicros: ULong 27 | 28 | /** 29 | * The maximum number of in-progress matches. 30 | * 31 | * Default: `UInt.MAX_VALUE` 32 | * 33 | * @throws [IllegalArgumentException] If the match limit is set to `0`. 34 | */ 35 | var matchLimit: UInt 36 | 37 | /** 38 | * The maximum start depth for the query. 39 | * 40 | * This prevents cursors from exploring children nodes at a certain depth. 41 | * Note that if a pattern includes many children, then they will still be checked. 42 | * 43 | * Default: `UInt.MAX_VALUE` 44 | */ 45 | var maxStartDepth: UInt 46 | 47 | /** 48 | * The range of bytes in which the query will be executed. 49 | * 50 | * Default: `UInt.MIN_VALUE..UInt.MAX_VALUE` 51 | */ 52 | var byteRange: UIntRange 53 | 54 | /** 55 | * The range of points in which the query will be executed. 56 | * 57 | * Default: `Point.MIN..Point.MAX` 58 | */ 59 | var pointRange: ClosedRange 60 | 61 | /** 62 | * Check if the query exceeded its maximum number of 63 | * in-progress matches during its last execution. 64 | */ 65 | val didExceedMatchLimit: Boolean 66 | 67 | /** 68 | * Iterate over all the matches in the order that they were found. 69 | * 70 | * #### Example 71 | * 72 | * ```kotlin 73 | * query.matches(tree.rootNode) { 74 | * if (name != "ieq?") return@matches true 75 | * val node = it[(args[0] as QueryPredicateArg.Capture).value].first() 76 | * val value = (args[1] as QueryPredicateArg.Literal).value 77 | * value.equals(node.text()?.toString(), ignoreCase = true) 78 | * } 79 | * ``` 80 | * 81 | * @param node The node that the query will run on. 82 | * @param predicate A function that handles custom predicates. 83 | */ 84 | fun matches( 85 | node: Node, 86 | predicate: QueryPredicate.(QueryMatch) -> Boolean = { true } 87 | ): Sequence 88 | 89 | /** 90 | * Iterate over all the individual captures in the order that they appear. 91 | * 92 | * This is useful if you don't care about _which_ pattern matched. 93 | * 94 | * @param node The node that the query will run on. 95 | * @param predicate A function that handles custom predicates. 96 | */ 97 | fun captures( 98 | node: Node, 99 | predicate: QueryPredicate.(QueryMatch) -> Boolean = { true } 100 | ): Sequence> 101 | 102 | /** 103 | * Get the property settings for the given pattern index. 104 | * 105 | * Properties are set using the `#set!` predicate. 106 | * 107 | * @return A map of properties with optional values. 108 | * @throws [IndexOutOfBoundsException] 109 | * If the index exceeds the [pattern count][patternCount]. 110 | */ 111 | fun settings(index: UInt): Map 112 | 113 | /** 114 | * Get the property assertions for the given pattern index. 115 | * 116 | * Assertions are performed using the `#is?` and `#is-not?` predicates. 117 | * 118 | * @return 119 | * A map of assertions, where the first item is the optional property value 120 | * and the second item indicates whether the assertion was positive or negative. 121 | * @throws [IndexOutOfBoundsException] 122 | * If the index exceeds the [pattern count][patternCount]. 123 | */ 124 | @Throws(IndexOutOfBoundsException::class) 125 | fun assertions(index: UInt): Map> 126 | 127 | /** 128 | * Disable a certain pattern within a query. 129 | * 130 | * This prevents the pattern from matching and removes most of the overhead 131 | * associated with the pattern. Currently, there is no way to undo this. 132 | * 133 | * @throws [IndexOutOfBoundsException] 134 | * If the index exceeds the [pattern count][patternCount]. 135 | */ 136 | @Throws(IndexOutOfBoundsException::class) 137 | fun disablePattern(index: UInt) 138 | 139 | /** 140 | * Disable a certain capture within a query. 141 | * 142 | * This prevents the capture from being returned in matches, 143 | * and also avoids most resource usage associated with recording 144 | * the capture. Currently, there is no way to undo this. 145 | * 146 | * @throws [NoSuchElementException] If the capture does not exist. 147 | */ 148 | @Throws(NoSuchElementException::class) 149 | fun disableCapture(name: String) 150 | 151 | /** 152 | * Get the byte offset where the given pattern starts in the query's source. 153 | * 154 | * @throws [IndexOutOfBoundsException] 155 | * If the index exceeds the [pattern count][patternCount]. 156 | */ 157 | @Throws(IndexOutOfBoundsException::class) 158 | fun startByteForPattern(index: UInt): UInt 159 | 160 | /** 161 | * Get the byte offset where the given pattern ends in the query's source. 162 | * 163 | * @throws [IndexOutOfBoundsException] 164 | * If the index exceeds the [pattern count][patternCount]. 165 | * @since 0.23.0 166 | */ 167 | @Throws(IndexOutOfBoundsException::class) 168 | fun endByteForPattern(index: UInt): UInt 169 | 170 | /** 171 | * Check if the pattern with the given index has a single root node. 172 | * 173 | * @throws [IndexOutOfBoundsException] 174 | * If the index exceeds the [pattern count][patternCount]. 175 | */ 176 | @Throws(IndexOutOfBoundsException::class) 177 | fun isPatternRooted(index: UInt): Boolean 178 | 179 | /** 180 | * Check if the pattern with the given index is "non-local". 181 | * 182 | * A non-local pattern has multiple root nodes and can match within a 183 | * repeating sequence of nodes, as specified by the grammar. Non-local 184 | * patterns disable certain optimizations that would otherwise be possible 185 | * when executing a query on a specific range of a syntax tree. 186 | * 187 | * @throws [IndexOutOfBoundsException] 188 | * If the index exceeds the [pattern count][patternCount]. 189 | */ 190 | @Throws(IndexOutOfBoundsException::class) 191 | fun isPatternNonLocal(index: UInt): Boolean 192 | 193 | /** 194 | * Check if a pattern is guaranteed to match once a given byte offset is reached. 195 | * 196 | * @throws [IndexOutOfBoundsException] If the offset exceeds the source length. 197 | */ 198 | @Throws(IndexOutOfBoundsException::class) 199 | fun isPatternGuaranteedAtStep(offset: UInt): Boolean 200 | } 201 | -------------------------------------------------------------------------------- /ktreesitter/src/commonMain/kotlin/io/github/treesitter/ktreesitter/QueryCapture.kt: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter 2 | 3 | import kotlin.jvm.JvmName 4 | 5 | /** 6 | * A [Node] that was captured with a certain capture [name]. 7 | * 8 | * @property node The captured node. 9 | * @property name The name of the capture. 10 | */ 11 | data class QueryCapture internal constructor( 12 | @get:JvmName("node") val node: Node, 13 | @get:JvmName("name") val name: String 14 | ) { 15 | override fun toString() = "QueryCapture(name=$name, node=$node)" 16 | } 17 | -------------------------------------------------------------------------------- /ktreesitter/src/commonMain/kotlin/io/github/treesitter/ktreesitter/QueryError.kt: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter 2 | 3 | /** Any error that occurred while instantiating a [Query]. */ 4 | sealed class QueryError : IllegalArgumentException() { 5 | abstract override val message: String 6 | 7 | /** A query syntax error. */ 8 | class Syntax(row: Long, column: Long) : QueryError() { 9 | override val message: String = 10 | if (row < 0 || column < 0) "Unexpected EOF" 11 | else "Invalid syntax at row $row, column $column" 12 | } 13 | 14 | /** A capture name error. */ 15 | class Capture(row: UInt, column: UInt, capture: String) : QueryError() { 16 | override val message: String = "Invalid capture name at row $row, column $column: $capture" 17 | } 18 | 19 | /** A field name error. */ 20 | class Field(row: UInt, column: UInt, field: String) : QueryError() { 21 | override val message: String = "Invalid field name at row $row, column $column: $field" 22 | } 23 | 24 | /** A node type error. */ 25 | class NodeType(row: UInt, column: UInt, type: String) : QueryError() { 26 | override val message: String = "Invalid node type at row $row, column $column: $type" 27 | } 28 | 29 | /** A pattern structure error. */ 30 | class Structure(row: UInt, column: UInt) : QueryError() { 31 | override val message: String = "Impossible pattern at row $row, column $column" 32 | } 33 | 34 | /** A query predicate error. */ 35 | class Predicate( 36 | row: UInt, 37 | details: String, 38 | override val cause: Throwable? = null 39 | ) : QueryError() { 40 | override val message = "Invalid predicate in pattern at row $row: $details" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ktreesitter/src/commonMain/kotlin/io/github/treesitter/ktreesitter/QueryMatch.kt: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter 2 | 3 | import kotlin.jvm.JvmName 4 | 5 | /** 6 | * A match that corresponds to a certain pattern in the query. 7 | * 8 | * @property patternIndex The index of the pattern. 9 | * @property captures The captures contained in the pattern. 10 | */ 11 | class QueryMatch internal constructor( 12 | @get:JvmName("getPatternIndex") val patternIndex: UInt, 13 | val captures: List 14 | ) { 15 | /** Get the nodes that are captured by the given [capture] name. */ 16 | operator fun get(capture: String): List = 17 | captures.mapNotNull { if (it.name == capture) it.node else null } 18 | 19 | override fun toString() = "QueryMatch(patternIndex=$patternIndex, captures=$captures)" 20 | } 21 | -------------------------------------------------------------------------------- /ktreesitter/src/commonMain/kotlin/io/github/treesitter/ktreesitter/QueryPredicate.kt: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter 2 | 3 | /** 4 | * A query [predicate](https://tree-sitter.github.io/tree-sitter/using-parsers#predicates) 5 | * that associates conditions or arbitrary metadata with a pattern. 6 | * 7 | * The following predicates are supported by default: 8 | * 9 | * - `#eq?`, `#not-eq?`, `#any-eq?`, `#any-not-eq?` 10 | * - `#match?`, `#not-match?`, `#any-match?`, `#any-not-match?` 11 | * - `#any-of?`, `#not-any-of?` 12 | * 13 | * @property name The name of the predicate. 14 | * @property args The arguments given to the predicate. 15 | */ 16 | sealed class QueryPredicate(val name: String) { 17 | abstract val args: List 18 | 19 | internal abstract operator fun invoke(match: QueryMatch): Boolean 20 | 21 | final override fun toString() = "QueryPredicate(name=$name, args=$args)" 22 | 23 | internal class EqCapture( 24 | name: String, 25 | private val capture: String, 26 | private val value: String, 27 | private val isPositive: Boolean, 28 | private val isAny: Boolean 29 | ) : QueryPredicate(name) { 30 | override val args = listOf( 31 | QueryPredicateArg.Capture(capture), 32 | QueryPredicateArg.Capture(value) 33 | ) 34 | 35 | override fun invoke(match: QueryMatch): Boolean { 36 | val nodes1 = match[capture] 37 | val nodes2 = match[value] 38 | val test = if (!isAny) nodes1::all else nodes1::any 39 | return test { n1 -> 40 | nodes2.any { n2 -> 41 | val result = n1.text() == n2.text() 42 | if (isPositive) result else !result 43 | } 44 | } 45 | } 46 | } 47 | 48 | internal class EqString( 49 | name: String, 50 | private val capture: String, 51 | private val value: String, 52 | private val isPositive: Boolean, 53 | private val isAny: Boolean 54 | ) : QueryPredicate(name) { 55 | override val args = listOf( 56 | QueryPredicateArg.Capture(capture), 57 | QueryPredicateArg.Literal(value) 58 | ) 59 | 60 | override fun invoke(match: QueryMatch): Boolean { 61 | val nodes = match[capture] 62 | if (nodes.isEmpty()) return !isPositive 63 | val test = if (!isAny) nodes::all else nodes::any 64 | return test { 65 | val result = value == it.text()!! 66 | if (isPositive) result else !result 67 | } 68 | } 69 | } 70 | 71 | internal class Match( 72 | name: String, 73 | private val capture: String, 74 | private val pattern: Regex, 75 | private val isPositive: Boolean, 76 | private val isAny: Boolean 77 | ) : QueryPredicate(name) { 78 | override val args = listOf( 79 | QueryPredicateArg.Capture(capture), 80 | QueryPredicateArg.Literal(pattern.pattern) 81 | ) 82 | 83 | override fun invoke(match: QueryMatch): Boolean { 84 | val nodes = match[capture] 85 | if (nodes.isEmpty()) return !isPositive 86 | val test = if (!isAny) nodes::all else nodes::any 87 | return test { 88 | val result = pattern.containsMatchIn(it.text()!!) 89 | if (isPositive) result else !result 90 | } 91 | } 92 | } 93 | 94 | internal class AnyOf( 95 | name: String, 96 | private val capture: String, 97 | private val value: List, 98 | private val isPositive: Boolean 99 | ) : QueryPredicate(name) { 100 | override val args = List(value.size + 1) { 101 | if (it == 0) QueryPredicateArg.Capture(capture) 102 | else QueryPredicateArg.Literal(value[it - 1]) 103 | } 104 | 105 | override fun invoke(match: QueryMatch) = 106 | match[capture].none { (it.text()!! in value) != isPositive } 107 | } 108 | 109 | internal class Generic( 110 | name: String, 111 | override val args: List 112 | ) : QueryPredicate(name) { 113 | override fun invoke(match: QueryMatch) = true 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /ktreesitter/src/commonMain/kotlin/io/github/treesitter/ktreesitter/QueryPredicateArg.kt: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter 2 | 3 | import kotlin.jvm.JvmInline 4 | 5 | /** 6 | * An argument to a [QueryPredicate]. 7 | * 8 | * @property value The value of the argument. 9 | */ 10 | sealed interface QueryPredicateArg { 11 | val value: String 12 | 13 | /** A capture argument (`@value`). */ 14 | @JvmInline 15 | value class Capture(override val value: String) : QueryPredicateArg { 16 | override fun toString() = "@$value" 17 | } 18 | 19 | /** A literal string argument (`"value"`). */ 20 | @JvmInline 21 | value class Literal(override val value: String) : QueryPredicateArg { 22 | override fun toString() = "\"$value\"" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ktreesitter/src/commonMain/kotlin/io/github/treesitter/ktreesitter/Range.kt: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter 2 | 3 | import kotlin.jvm.JvmName 4 | 5 | /** 6 | * A range of positions in a text document, 7 | * both in terms of bytes and of row-column points. 8 | * 9 | * @constructor 10 | * @throws [IllegalArgumentException] 11 | * If the end point is smaller than the start point, 12 | * or the end byte is smaller than the start byte. 13 | */ 14 | data class Range @Throws(IllegalArgumentException::class) constructor( 15 | @get:JvmName("startPoint") val startPoint: Point, 16 | @get:JvmName("endPoint") val endPoint: Point, 17 | @get:JvmName("startByte") val startByte: UInt, 18 | @get:JvmName("endByte") val endByte: UInt 19 | ) { 20 | init { 21 | require(startPoint <= endPoint) { "Invalid point range: $startPoint to $endPoint" } 22 | require(startByte <= endByte) { "Invalid byte range: $startByte to $endByte" } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ktreesitter/src/commonMain/kotlin/io/github/treesitter/ktreesitter/Tree.kt: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter 2 | 3 | /** A class that represents a syntax tree. */ 4 | expect class Tree { 5 | /** The root node of the syntax tree. */ 6 | val rootNode: Node 7 | 8 | /** The language that was used to parse the syntax tree. */ 9 | val language: Language 10 | 11 | /** The included ranges that were used to parse the syntax tree. */ 12 | val includedRanges: List 13 | 14 | /** 15 | * Get the root node of the syntax tree, but with 16 | * its position shifted forward by the given offset. 17 | */ 18 | fun rootNodeWithOffset(bytes: UInt, extent: Point): Node? 19 | 20 | /** 21 | * Edit the syntax tree to keep it in sync 22 | * with source code that has been modified. 23 | */ 24 | fun edit(edit: InputEdit) 25 | 26 | /** 27 | * Create a shallow copy of the syntax tree. 28 | * 29 | * You need to copy a syntax tree in order to use it on multiple 30 | * threads or coroutines, as syntax trees are not thread safe. 31 | */ 32 | fun copy(): Tree 33 | 34 | /** Create a new tree cursor starting from the node of the tree. */ 35 | fun walk(): TreeCursor 36 | 37 | /** Get the source code of the syntax tree, if available. */ 38 | fun text(): CharSequence? 39 | 40 | /** 41 | * Compare an old edited syntax tree to a new 42 | * syntax tree representing the same document. 43 | * 44 | * For this to work correctly, this tree must have been 45 | * edited such that its ranges match up to the new tree. 46 | * 47 | * @return A list of ranges whose syntactic structure has changed. 48 | */ 49 | fun changedRanges(newTree: Tree): List 50 | } 51 | -------------------------------------------------------------------------------- /ktreesitter/src/commonMain/kotlin/io/github/treesitter/ktreesitter/TreeCursor.kt: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter 2 | 3 | /** A class that can be used to efficiently walk a [syntax tree][Tree]. */ 4 | expect class TreeCursor { 5 | internal val tree: Tree 6 | 7 | /** The current node of the cursor. */ 8 | val currentNode: Node 9 | 10 | /** 11 | * The depth of the cursor's current node relative to the 12 | * original node that the cursor was constructed with. 13 | */ 14 | val currentDepth: UInt 15 | 16 | /** 17 | * The field ID of the tree cursor's current node, or `0`. 18 | * 19 | * @see [Node.childByFieldId] 20 | * @see [Language.fieldIdForName] 21 | */ 22 | val currentFieldId: UShort 23 | 24 | /** 25 | * The field name of the tree cursor's current node, if available. 26 | * 27 | * @see [Node.childByFieldName] 28 | */ 29 | val currentFieldName: String? 30 | 31 | /** 32 | * The index of the cursor's current node out of all the descendants 33 | * of the original node that the cursor was constructed with. 34 | */ 35 | val currentDescendantIndex: UInt 36 | 37 | /** Create a shallow copy of the tree cursor. */ 38 | fun copy(): TreeCursor 39 | 40 | /** Reset the cursor to start at a different node. */ 41 | fun reset(node: Node) 42 | 43 | /** Reset the cursor to start at the same position as another cursor. */ 44 | fun reset(cursor: TreeCursor) 45 | 46 | /** 47 | * Move the cursor to the first child of its current node. 48 | * 49 | * @return 50 | * `true` if the cursor successfully moved, 51 | * or `false` if there were no children. 52 | */ 53 | fun gotoFirstChild(): Boolean 54 | 55 | /** 56 | * Move the cursor to the last child of its current node. 57 | * 58 | * @return 59 | * `true` if the cursor successfully moved, 60 | * or `false` if there were no children. 61 | */ 62 | fun gotoLastChild(): Boolean 63 | 64 | /** 65 | * Move the cursor to the parent of its current node. 66 | * 67 | * @return 68 | * `true` if the cursor successfully moved, 69 | * or `false` if there was no parent node. 70 | */ 71 | fun gotoParent(): Boolean 72 | 73 | /** 74 | * Move the cursor to the next sibling of its current node. 75 | * 76 | * @return 77 | * `true` if the cursor successfully moved, 78 | * or `false` if there was no next sibling node. 79 | */ 80 | fun gotoNextSibling(): Boolean 81 | 82 | /** 83 | * Move the cursor to the previous sibling of its current node. 84 | * 85 | * This function may be slower than [gotoNextSibling] due to how node positions 86 | * are stored. In the worst case, this will need to iterate through all the 87 | * children up to the previous sibling node to recalculate its position. 88 | * 89 | * @return 90 | * `true` if the cursor successfully moved, 91 | * or `false` if there was no previous sibling node. 92 | */ 93 | fun gotoPreviousSibling(): Boolean 94 | 95 | /** 96 | * Move the cursor to the node that is the nth descendant of 97 | * the original node that the cursor was constructed with, 98 | * where `0` represents the original node itself. 99 | */ 100 | fun gotoDescendant(index: UInt) 101 | 102 | /** 103 | * Move the cursor to the first child of its current 104 | * node that extends beyond the given byte offset. 105 | * 106 | * @return The index of the child node, or `null` if no such child was found. 107 | */ 108 | fun gotoFirstChildForByte(byte: UInt): UInt? 109 | 110 | /** 111 | * Move the cursor to the first child of its current 112 | * node that extends beyond the given point offset. 113 | * 114 | * @return The index of the child node, or `null` if no such child was found. 115 | */ 116 | fun gotoFirstChildForPoint(point: Point): UInt? 117 | } 118 | -------------------------------------------------------------------------------- /ktreesitter/src/commonMain/kotlin/io/github/treesitter/ktreesitter/TreeSitter.kt: -------------------------------------------------------------------------------- 1 | @file:kotlin.jvm.JvmName("KTreeSitter") 2 | 3 | package io.github.treesitter.ktreesitter 4 | 5 | // NOTE: don't forget to bump these when necessary 6 | 7 | /** 8 | * The latest ABI version that is supported by the current version of the library. 9 | * 10 | * The Tree-sitter library is generally backwards-compatible with languages 11 | * generated using older CLI versions, but is not forwards-compatible. 12 | */ 13 | const val LANGUAGE_VERSION: UInt = 14U 14 | 15 | /** The earliest ABI version that is supported by the current version of the library. */ 16 | const val MIN_COMPATIBLE_LANGUAGE_VERSION: UInt = 13U 17 | -------------------------------------------------------------------------------- /ktreesitter/src/commonTest/kotlin/io/github/treesitter/ktreesitter/LanguageTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter 2 | 3 | import io.github.treesitter.ktreesitter.java.TreeSitterJava 4 | import io.kotest.assertions.throwables.shouldNotThrowAny 5 | import io.kotest.core.spec.style.FunSpec 6 | import io.kotest.matchers.* 7 | import io.kotest.matchers.comparables.* 8 | import io.kotest.matchers.nulls.* 9 | import io.kotest.matchers.string.* 10 | import io.kotest.matchers.types.* 11 | 12 | class LanguageTest : FunSpec({ 13 | val language = Language(TreeSitterJava.language()) 14 | 15 | test("version") { 16 | language.version shouldBe 14U 17 | } 18 | 19 | test("symbolCount") { 20 | language.symbolCount shouldBeGreaterThan 1U 21 | } 22 | 23 | test("stateCount") { 24 | language.stateCount shouldBeGreaterThan 1U 25 | } 26 | 27 | test("fieldCount") { 28 | language.fieldCount shouldBeGreaterThan 1U 29 | } 30 | 31 | test("symbolName()") { 32 | language.symbolName(1U) shouldBe "identifier" 33 | } 34 | 35 | test("symbolForName()") { 36 | language.symbolForName(";", false) shouldBeGreaterThan 0U 37 | language.symbolForName("program", true) shouldBeGreaterThan 0U 38 | } 39 | 40 | test("isNamed()") { 41 | language.isNamed(1U) shouldBe true 42 | } 43 | 44 | test("isVisible()") { 45 | language.isVisible(1U) shouldBe true 46 | } 47 | 48 | test("isSupertype()") { 49 | language.isSupertype(1U) shouldBe false 50 | } 51 | 52 | test("fieldNameForId()") { 53 | language.fieldNameForId(1U).shouldNotBeNull() 54 | } 55 | 56 | test("fieldIdForName()") { 57 | language.fieldIdForName("body") shouldBeGreaterThan 0U 58 | } 59 | 60 | test("nextState()") { 61 | val program = language.symbolForName("program", true) 62 | language.nextState(1U, program) shouldBeGreaterThan 0U 63 | } 64 | 65 | test("lookaheadIterator()") { 66 | val program = language.symbolForName("program", true) 67 | val state = language.nextState(1U, program) 68 | val lookahead = language.lookaheadIterator(state) 69 | lookahead.language shouldBe language 70 | } 71 | 72 | test("query()") { 73 | shouldNotThrowAny { language.query("(program) @root") } 74 | } 75 | 76 | test("equals()") { 77 | Language(TreeSitterJava.language()) shouldBe language.copy() 78 | } 79 | 80 | test("hashCode()") { 81 | language shouldHaveSameHashCodeAs TreeSitterJava.language() 82 | } 83 | 84 | test("toString()") { 85 | language.toString() shouldMatch Regex("""Language\(id=0x[0-9a-f]+, version=14\)""") 86 | } 87 | }) 88 | -------------------------------------------------------------------------------- /ktreesitter/src/commonTest/kotlin/io/github/treesitter/ktreesitter/LookaheadIteratorTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter 2 | 3 | import io.github.treesitter.ktreesitter.java.TreeSitterJava 4 | import io.kotest.core.spec.style.FunSpec 5 | import io.kotest.matchers.* 6 | import io.kotest.matchers.collections.* 7 | 8 | class LookaheadIteratorTest : FunSpec({ 9 | val language = Language(TreeSitterJava.language()) 10 | val state = language.nextState(1U, 138U) 11 | val lookahead = language.lookaheadIterator(state) 12 | 13 | test("currentSymbol") { 14 | lookahead.currentSymbol shouldBe UShort.MAX_VALUE 15 | lookahead.currentSymbolName shouldBe "ERROR" 16 | } 17 | 18 | test("next()") { 19 | lookahead.next() shouldBe LookaheadIterator.Symbol(0U, "end") 20 | } 21 | 22 | test("reset()") { 23 | lookahead.reset(state) shouldBe true 24 | lookahead.reset(state, language) shouldBe true 25 | } 26 | 27 | test("symbols()") { 28 | lookahead.symbols().shouldBeStrictlyIncreasing() 29 | } 30 | 31 | test("names()") { 32 | val names = lookahead.symbolNames().toList() 33 | names shouldContainExactly listOf("end", "line_comment", "block_comment") 34 | } 35 | 36 | test("iterator") { 37 | for ((symbol, name) in lookahead) { 38 | symbol shouldBe lookahead.currentSymbol 39 | name shouldBe lookahead.currentSymbolName 40 | } 41 | } 42 | }) 43 | -------------------------------------------------------------------------------- /ktreesitter/src/commonTest/kotlin/io/github/treesitter/ktreesitter/NodeTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter 2 | 3 | import io.github.treesitter.ktreesitter.java.TreeSitterJava 4 | import io.kotest.assertions.throwables.shouldThrow 5 | import io.kotest.core.spec.style.FunSpec 6 | import io.kotest.inspectors.forSingle 7 | import io.kotest.matchers.* 8 | import io.kotest.matchers.collections.* 9 | import io.kotest.matchers.nulls.* 10 | import io.kotest.matchers.types.* 11 | 12 | class NodeTest : FunSpec({ 13 | val language = Language(TreeSitterJava.language()) 14 | val parser = Parser(language) 15 | val source = "class Foo {}" 16 | val tree = parser.parse(source) 17 | val rootNode = tree.rootNode 18 | 19 | test("id") { 20 | rootNode.id shouldNotBe 0UL 21 | } 22 | 23 | test("symbol") { 24 | rootNode.symbol shouldNotBe UShort.MIN_VALUE 25 | } 26 | 27 | test("grammarSymbol") { 28 | rootNode.grammarSymbol shouldBe rootNode.symbol 29 | } 30 | 31 | test("type") { 32 | rootNode.type shouldBe "program" 33 | } 34 | 35 | test("grammarType") { 36 | rootNode.grammarType shouldBe rootNode.type 37 | } 38 | 39 | test("isNamed") { 40 | rootNode.isNamed shouldBe true 41 | } 42 | 43 | test("isExtra") { 44 | rootNode.isExtra shouldBe false 45 | } 46 | 47 | test("isError") { 48 | rootNode.isError shouldBe false 49 | } 50 | 51 | test("isMissing") { 52 | rootNode.isMissing shouldBe false 53 | } 54 | 55 | test("hasChanges") { 56 | rootNode.hasChanges shouldBe false 57 | } 58 | 59 | test("hasError") { 60 | rootNode.hasError shouldBe false 61 | } 62 | 63 | test("parseState") { 64 | rootNode.parseState shouldBe 0U 65 | } 66 | 67 | test("nextParseState") { 68 | rootNode.nextParseState shouldBe 0U 69 | } 70 | 71 | test("startByte") { 72 | rootNode.startByte shouldBe 0U 73 | } 74 | 75 | test("endByte") { 76 | rootNode.endByte shouldBe 12U 77 | } 78 | 79 | test("byteRange") { 80 | rootNode.byteRange shouldBe 0U..12U 81 | } 82 | 83 | test("range") { 84 | rootNode.range shouldBe Range(Point(0U, 0U), Point(0U, 12U), 0U, 12U) 85 | } 86 | 87 | test("startPoint") { 88 | rootNode.startPoint shouldBe Point(0U, 0U) 89 | } 90 | 91 | test("endPoint") { 92 | rootNode.endPoint shouldBe Point(0U, 12U) 93 | } 94 | 95 | test("childCount") { 96 | rootNode.childCount shouldBe 1U 97 | } 98 | 99 | test("namedChildCount") { 100 | rootNode.namedChildCount shouldBe 1U 101 | } 102 | 103 | test("descendantCount") { 104 | rootNode.descendantCount shouldBe 7U 105 | } 106 | 107 | test("parent") { 108 | rootNode.parent.shouldBeNull() 109 | } 110 | 111 | test("nextSibling") { 112 | rootNode.nextSibling.shouldBeNull() 113 | } 114 | 115 | test("prevSibling") { 116 | rootNode.prevSibling.shouldBeNull() 117 | } 118 | 119 | test("nextNamedSibling") { 120 | rootNode.nextNamedSibling.shouldBeNull() 121 | } 122 | 123 | test("prevNamedSibling") { 124 | rootNode.prevNamedSibling.shouldBeNull() 125 | } 126 | 127 | test("children") { 128 | val children = rootNode.children 129 | children.forSingle { it.type shouldBe "class_declaration" } 130 | rootNode.children shouldBeSameInstanceAs children 131 | } 132 | 133 | test("namedChildren") { 134 | val children = rootNode.namedChildren 135 | children shouldContainExactly rootNode.children 136 | } 137 | 138 | test("child()") { 139 | val node = rootNode.child(0U) 140 | node?.type shouldBe "class_declaration" 141 | shouldThrow { rootNode.child(1U) } 142 | } 143 | 144 | test("namedChild()") { 145 | rootNode.namedChild(0U) shouldBe rootNode.child(0U) 146 | shouldThrow { rootNode.namedChild(1U) } 147 | } 148 | 149 | test("childByFieldId()") { 150 | rootNode.childByFieldId(0U).shouldBeNull() 151 | } 152 | 153 | test("childByFieldName()") { 154 | val child = rootNode.child(0U)!!.childByFieldName("body") 155 | child?.type shouldBe "class_body" 156 | } 157 | 158 | test("childrenByFieldId()") { 159 | val id = language.fieldIdForName("name") 160 | val children = rootNode.child(0U)!!.childrenByFieldId(id) 161 | children.forSingle { it.type shouldBe "identifier" } 162 | } 163 | 164 | test("childrenByFieldName()") { 165 | val children = rootNode.child(0U)!!.childrenByFieldName("name") 166 | children.forSingle { it.type shouldBe "identifier" } 167 | } 168 | 169 | test("fieldNameForChild()") { 170 | rootNode.child(0U)!!.fieldNameForChild(2U) shouldBe "body" 171 | } 172 | 173 | test("fieldNameForNamedChild()") { 174 | rootNode.child(0U)!!.fieldNameForNamedChild(2U).shouldBeNull() 175 | } 176 | 177 | @Suppress("DEPRECATION") 178 | test("childContainingDescendant()") { 179 | val descendant = rootNode.child(0U)!!.child(0U)!! 180 | val child = rootNode.childContainingDescendant(descendant) 181 | child?.type shouldBe "class_declaration" 182 | } 183 | 184 | test("childWithDescendant()") { 185 | val descendant = rootNode.child(0U)!! 186 | val child = rootNode.childWithDescendant(descendant) 187 | child?.type shouldBe "class_declaration" 188 | } 189 | 190 | test("descendant()") { 191 | rootNode.descendant(0U, 5U)?.type shouldBe "class" 192 | rootNode.descendant(Point(0U, 10U), Point(0U, 12U))?.type shouldBe "class_body" 193 | } 194 | 195 | test("namedDescendant()") { 196 | rootNode.namedDescendant(0U, 5U)?.type shouldBe "class_declaration" 197 | rootNode.namedDescendant(Point(0U, 6U), Point(0U, 9U))?.type shouldBe "identifier" 198 | } 199 | 200 | test("walk()") { 201 | val cursor = rootNode.walk() 202 | cursor.currentNode shouldBeSameInstanceAs rootNode 203 | } 204 | 205 | test("copy()") { 206 | val copy = tree.copy() 207 | copy shouldNotBeSameInstanceAs tree 208 | copy.text() shouldBe tree.text() 209 | } 210 | 211 | @Suppress("UnnecessaryOptInAnnotation") 212 | @OptIn(ExperimentalMultiplatform::class) 213 | test("edit()") { 214 | val edit = InputEdit(0U, 12U, 10U, Point(0U, 0U), Point(0U, 12U), Point(0U, 10U)) 215 | val copy = tree.copy() 216 | val node = copy.rootNode 217 | copy.edit(edit) 218 | node.edit(edit) 219 | node.hasChanges shouldBe true 220 | } 221 | 222 | test("text()") { 223 | rootNode.descendant(6U, 9U)?.text() shouldBe "Foo" 224 | } 225 | 226 | test("sexp()") { 227 | rootNode.child(0U)!!.sexp() shouldBe 228 | "(class_declaration name: (identifier) body: (class_body))" 229 | } 230 | 231 | test("equals()") { 232 | rootNode shouldNotBe rootNode.child(0U) 233 | } 234 | 235 | test("hashCode()") { 236 | rootNode.hashCode() shouldBe rootNode.id.toInt() 237 | } 238 | 239 | test("toString()") { 240 | rootNode.toString() shouldBe "Node(type=program, startByte=0, endByte=12)" 241 | } 242 | }) 243 | -------------------------------------------------------------------------------- /ktreesitter/src/commonTest/kotlin/io/github/treesitter/ktreesitter/ParserTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter 2 | 3 | import io.github.treesitter.ktreesitter.java.TreeSitterJava 4 | import io.kotest.assertions.throwables.shouldNotThrowAnyUnit 5 | import io.kotest.core.spec.style.FunSpec 6 | import io.kotest.inspectors.forSome 7 | import io.kotest.matchers.* 8 | import io.kotest.matchers.collections.* 9 | import io.kotest.matchers.nulls.* 10 | import io.kotest.matchers.string.* 11 | import io.kotest.matchers.types.* 12 | 13 | class ParserTest : FunSpec({ 14 | val parser = Parser() 15 | 16 | test("language") { 17 | val language = Language(TreeSitterJava.language()) 18 | parser.language.shouldBeNull() 19 | parser.language = language 20 | parser.language shouldBeSameInstanceAs language 21 | } 22 | 23 | test("includedRanges") { 24 | val range = Range(Point(0U, 0U), Point(0U, 1U), 0U, 1U) 25 | parser.includedRanges.shouldBeEmpty() 26 | parser.includedRanges = listOf(range) 27 | parser.includedRanges shouldHaveSingleElement range 28 | } 29 | 30 | test("timeoutMicros") { 31 | parser.timeoutMicros shouldBe 0UL 32 | parser.timeoutMicros = 10000UL 33 | parser.timeoutMicros shouldBe 10000UL 34 | } 35 | 36 | test("logger") { 37 | shouldNotThrowAnyUnit { 38 | parser.logger = { _, _ -> 39 | throw UnsupportedOperationException() 40 | } 41 | } 42 | } 43 | 44 | test("parse(source)") { 45 | // UTF-8 46 | var source = "class Foo {}" 47 | var tree = parser.parse(source) 48 | tree.text()?.get(5) shouldBe ' ' 49 | 50 | // logging 51 | val logs = mutableListOf() 52 | parser.logger = { type, msg -> logs += "$type - $msg" } 53 | parser.reset() 54 | parser.parse(source) 55 | parser.logger = null 56 | logs.shouldNotBeEmpty().forSome { 57 | it shouldStartWith "LEX" 58 | } 59 | 60 | // UTF-16 61 | source = "var java = \"💩\"" 62 | tree = parser.parse(source) 63 | tree.text()?.subSequence(12, 14) shouldBe "\uD83D\uDCA9" 64 | } 65 | 66 | test("parse(callback)") { 67 | val source = "class Foo {}" 68 | val tree = parser.parse { byte, _ -> 69 | val end = minOf(byte.toInt() * 2, source.length) 70 | source.subSequence(byte.toInt(), end).ifEmpty { null } 71 | } 72 | tree.text().shouldBeNull() 73 | tree.rootNode.type shouldBe "program" 74 | } 75 | 76 | afterTest { (test, _) -> 77 | when (test.name.testName) { 78 | "includedRanges" -> parser.includedRanges = emptyList() 79 | "timeoutMicros" -> parser.timeoutMicros = 0UL 80 | "logger" -> parser.logger = null 81 | } 82 | } 83 | }) 84 | -------------------------------------------------------------------------------- /ktreesitter/src/commonTest/kotlin/io/github/treesitter/ktreesitter/TreeCursorTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter 2 | 3 | import io.github.treesitter.ktreesitter.java.TreeSitterJava 4 | import io.kotest.core.spec.style.FunSpec 5 | import io.kotest.matchers.* 6 | import io.kotest.matchers.nulls.* 7 | import io.kotest.matchers.types.* 8 | 9 | class TreeCursorTest : FunSpec({ 10 | val language = Language(TreeSitterJava.language()) 11 | val parser = Parser(language) 12 | val source = "class Foo {}" 13 | val tree = parser.parse(source) 14 | val cursor = tree.walk() 15 | val rootNode = tree.rootNode 16 | 17 | test("currentNode") { 18 | val node = cursor.currentNode 19 | node shouldBe rootNode 20 | node shouldBeSameInstanceAs cursor.currentNode 21 | } 22 | 23 | test("currentDepth") { 24 | cursor.currentDepth shouldBe 0U 25 | } 26 | 27 | test("currentFieldId") { 28 | cursor.currentFieldId shouldBe 0U 29 | } 30 | 31 | test("currentFieldName") { 32 | cursor.currentFieldName.shouldBeNull() 33 | } 34 | 35 | test("currentDescendantIndex") { 36 | cursor.currentDescendantIndex shouldBe 0U 37 | } 38 | 39 | test("copy()") { 40 | val copy = cursor.copy() 41 | copy shouldNotBeSameInstanceAs cursor 42 | cursor.reset(copy) 43 | copy.currentNode shouldBe cursor.currentNode 44 | } 45 | 46 | test("gotoFirstChild()") { 47 | cursor.gotoFirstChild() shouldBe true 48 | cursor.currentNode.type shouldBe "class_declaration" 49 | } 50 | 51 | test("gotoLastChild()") { 52 | cursor.gotoLastChild() shouldBe true 53 | cursor.currentFieldName shouldBe "body" 54 | } 55 | 56 | test("gotoParent()") { 57 | cursor.gotoParent() shouldBe true 58 | cursor.currentNode.type shouldBe "class_declaration" 59 | } 60 | 61 | test("gotoNextSibling()") { 62 | cursor.gotoNextSibling() shouldBe false 63 | } 64 | 65 | test("gotoPreviousSibling()") { 66 | cursor.gotoPreviousSibling() shouldBe false 67 | } 68 | 69 | test("gotoDescendant()") { 70 | cursor.gotoDescendant(2U) 71 | cursor.currentDescendantIndex shouldBe 2U 72 | cursor.reset(rootNode) 73 | } 74 | 75 | test("gotoFirstChildForByte()") { 76 | cursor.gotoFirstChildForByte(1U) shouldBe 0U 77 | cursor.currentNode.type shouldBe "class_declaration" 78 | } 79 | 80 | test("gotoFirstChildForPoint()") { 81 | cursor.gotoFirstChildForPoint(Point(0U, 7U)) shouldBe 1U 82 | cursor.currentFieldName shouldBe "name" 83 | } 84 | }) 85 | -------------------------------------------------------------------------------- /ktreesitter/src/commonTest/kotlin/io/github/treesitter/ktreesitter/TreeTest.kt: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter 2 | 3 | import io.github.treesitter.ktreesitter.java.TreeSitterJava 4 | import io.kotest.core.spec.style.FunSpec 5 | import io.kotest.matchers.* 6 | import io.kotest.matchers.collections.* 7 | import io.kotest.matchers.nulls.* 8 | import io.kotest.matchers.types.* 9 | 10 | class TreeTest : FunSpec({ 11 | val language = Language(TreeSitterJava.language()) 12 | val parser = Parser(language) 13 | var source = "class Foo {}" 14 | var tree = parser.parse(source) 15 | 16 | test("language") { 17 | tree.language shouldBeSameInstanceAs language 18 | } 19 | 20 | test("rootNode") { 21 | tree.rootNode.endByte shouldBe source.length.toUInt() 22 | } 23 | 24 | test("includedRanges") { 25 | tree.includedRanges.shouldHaveSingleElement { 26 | it.startByte == 0U && it.endByte == UInt.MAX_VALUE 27 | } 28 | } 29 | 30 | test("rootNodeWithOffset()") { 31 | val node = tree.rootNodeWithOffset(6U, Point(0U, 6U)) 32 | node?.text() shouldBe "Foo {}" 33 | } 34 | 35 | test("text()") { 36 | tree.text() shouldBe source 37 | } 38 | 39 | test("edit()") { 40 | val edit = InputEdit(9U, 9U, 10U, Point(0U, 9U), Point(0U, 9U), Point(0U, 10U)) 41 | source = "class Foo2 {}" 42 | tree.edit(edit) 43 | tree.text().shouldBeNull() 44 | tree = parser.parse(source, tree) 45 | tree.text() shouldBe source 46 | } 47 | 48 | test("walk()") { 49 | val cursor = tree.walk() 50 | cursor.tree shouldBeSameInstanceAs tree 51 | } 52 | 53 | test("changedRanges()") { 54 | val edit = InputEdit(0U, 0U, 7U, Point(0U, 0U), Point(0U, 0U), Point(0U, 7U)) 55 | tree.edit(edit) 56 | val newTree = parser.parse("public $source", tree) 57 | tree.changedRanges(newTree).shouldHaveSingleElement { 58 | it.endByte == 7U && it.endPoint.column == 7U 59 | } 60 | } 61 | }) 62 | -------------------------------------------------------------------------------- /ktreesitter/src/commonTest/resources/kotest.properties: -------------------------------------------------------------------------------- 1 | # suppress inspection "UnusedProperty" for whole file 2 | kotest.framework.classpath.scanning.config.disable=true 3 | kotest.framework.classpath.scanning.autoscan.disable=true 4 | kotest.framework.assertion.globalassertsoftly=true 5 | -------------------------------------------------------------------------------- /ktreesitter/src/jni/language.c: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | jlong JNICALL language_copy CRITICAL_ARGS(jlong self) { 4 | return (jlong)ts_language_copy((TSLanguage *)self); 5 | } 6 | 7 | jint JNICALL language_get_version(JNIEnv *env, jobject this) { 8 | TSLanguage *self = GET_POINTER(TSLanguage, this, Language_self); 9 | return (jint)ts_language_version(self); 10 | } 11 | 12 | jint JNICALL language_get_symbol_count(JNIEnv *env, jobject this) { 13 | TSLanguage *self = GET_POINTER(TSLanguage, this, Language_self); 14 | return (jint)ts_language_symbol_count(self); 15 | } 16 | 17 | jint JNICALL language_get_state_count(JNIEnv *env, jobject this) { 18 | TSLanguage *self = GET_POINTER(TSLanguage, this, Language_self); 19 | return (jint)ts_language_state_count(self); 20 | } 21 | 22 | jint JNICALL language_get_field_count(JNIEnv *env, jobject this) { 23 | TSLanguage *self = GET_POINTER(TSLanguage, this, Language_self); 24 | return (jint)ts_language_field_count(self); 25 | } 26 | 27 | jstring JNICALL language_symbol_name(JNIEnv *env, jobject this, jshort symbol) { 28 | TSLanguage *self = GET_POINTER(TSLanguage, this, Language_self); 29 | const char *name = ts_language_symbol_name(self, (uint16_t)symbol); 30 | return (*env)->NewStringUTF(env, name); 31 | } 32 | 33 | jshort JNICALL language_symbol_for_name(JNIEnv *env, jobject this, jstring name, 34 | jboolean is_named) { 35 | TSLanguage *self = GET_POINTER(TSLanguage, this, Language_self); 36 | const char *symbol_name = (*env)->GetStringUTFChars(env, name, NULL); 37 | uint32_t length = (uint32_t)(*env)->GetStringUTFLength(env, name); 38 | uint16_t symbol = ts_language_symbol_for_name(self, symbol_name, length, (bool)is_named); 39 | (*env)->ReleaseStringUTFChars(env, name, symbol_name); 40 | return (jshort)symbol; 41 | } 42 | 43 | jboolean JNICALL language_is_named(JNIEnv *env, jobject this, jshort symbol) { 44 | TSLanguage *self = GET_POINTER(TSLanguage, this, Language_self); 45 | TSSymbolType symbol_type = ts_language_symbol_type(self, symbol); 46 | return (jboolean)(symbol_type == TSSymbolTypeRegular); 47 | } 48 | 49 | jboolean JNICALL language_is_visible(JNIEnv *env, jobject this, jshort symbol) { 50 | TSLanguage *self = GET_POINTER(TSLanguage, this, Language_self); 51 | TSSymbolType symbol_type = ts_language_symbol_type(self, symbol); 52 | return (jboolean)(symbol_type <= TSSymbolTypeAnonymous); 53 | } 54 | 55 | jboolean JNICALL language_is_supertype(JNIEnv *env, jobject this, jshort symbol) { 56 | TSLanguage *self = GET_POINTER(TSLanguage, this, Language_self); 57 | TSSymbolType symbol_type = ts_language_symbol_type(self, symbol); 58 | return (jboolean)(symbol_type == TSSymbolTypeSupertype); 59 | } 60 | 61 | jstring JNICALL language_field_name_for_id(JNIEnv *env, jobject this, jshort id) { 62 | TSLanguage *self = GET_POINTER(TSLanguage, this, Language_self); 63 | const char *name = ts_language_field_name_for_id(self, (uint16_t)id); 64 | return name ? (*env)->NewStringUTF(env, name) : NULL; 65 | } 66 | 67 | jint JNICALL language_field_id_for_name(JNIEnv *env, jobject this, jstring name) { 68 | TSLanguage *self = GET_POINTER(TSLanguage, this, Language_self); 69 | const char *field_name = (*env)->GetStringUTFChars(env, name, NULL); 70 | uint32_t length = (uint32_t)(*env)->GetStringUTFLength(env, name); 71 | uint16_t field_id = ts_language_field_id_for_name(self, field_name, length); 72 | (*env)->ReleaseStringUTFChars(env, name, field_name); 73 | return (jint)field_id; 74 | } 75 | 76 | jshort JNICALL language_next_state(JNIEnv *env, jobject this, jshort state, jshort symbol) { 77 | TSLanguage *self = GET_POINTER(TSLanguage, this, Language_self); 78 | return (jshort)ts_language_next_state(self, (uint16_t)state, (uint16_t)symbol); 79 | } 80 | 81 | void JNICALL language_check_version(JNIEnv *env, jobject this) { 82 | TSLanguage *self = GET_POINTER(TSLanguage, this, Language_self); 83 | uint32_t version = ts_language_version(self); 84 | if (version < TREE_SITTER_MIN_COMPATIBLE_LANGUAGE_VERSION || 85 | version > TREE_SITTER_LANGUAGE_VERSION) { 86 | const char *fmt = "Incompatible language version %u. Must be between %u and %u."; 87 | char buffer[70] = {0}; // length(fmt) + digits(UINT32_MAX) 88 | sprintf_s(buffer, 70, fmt, version, TREE_SITTER_MIN_COMPATIBLE_LANGUAGE_VERSION, 89 | TREE_SITTER_LANGUAGE_VERSION); 90 | THROW(IllegalArgumentException, (const char *)buffer); 91 | } 92 | } 93 | 94 | const JNINativeMethod Language_methods[] = { 95 | {"copy", "(J)J", (void *)&language_copy}, 96 | {"getVersion", "()I", (void *)&language_get_version}, 97 | {"getSymbolCount", "()I", (void *)&language_get_symbol_count}, 98 | {"getStateCount", "()I", (void *)&language_get_state_count}, 99 | {"getFieldCount", "()I", (void *)&language_get_field_count}, 100 | {"symbolName", "(S)Ljava/lang/String;", (void *)&language_symbol_name}, 101 | {"symbolForName", "(Ljava/lang/String;Z)S", (void *)&language_symbol_for_name}, 102 | {"isNamed", "(S)Z", (void *)&language_is_named}, 103 | {"isVisible", "(S)Z", (void *)&language_is_visible}, 104 | {"isSupertype", "(S)Z", (void *)&language_is_supertype}, 105 | {"fieldNameForId", "(S)Ljava/lang/String;", (void *)&language_field_name_for_id}, 106 | {"fieldIdForName", "(Ljava/lang/String;)S", (void *)&language_field_id_for_name}, 107 | {"nextState", "(SS)S", (void *)&language_next_state}, 108 | {"checkVersion", "()V", (void *)&language_check_version}, 109 | }; 110 | 111 | const size_t Language_methods_size = sizeof Language_methods / sizeof(JNINativeMethod); 112 | -------------------------------------------------------------------------------- /ktreesitter/src/jni/lookahead_iterator.c: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | jlong JNICALL lookahead_iterator_init CRITICAL_ARGS(jlong language, jshort state) { 4 | TSLookaheadIterator *lookahead = 5 | ts_lookahead_iterator_new((TSLanguage *)language, (uint16_t)state); 6 | return (jlong)lookahead; 7 | } 8 | 9 | void JNICALL lookahead_iterator_delete CRITICAL_ARGS(jlong self) { 10 | ts_lookahead_iterator_delete((TSLookaheadIterator *)self); 11 | } 12 | 13 | jobject JNICALL lookahead_iterator_get_language(JNIEnv *env, jobject this) { 14 | TSLookaheadIterator *self = GET_POINTER(TSLookaheadIterator, this, LookaheadIterator_self); 15 | const TSLanguage *language = ts_lookahead_iterator_language(self); 16 | jobject language_obj = (*env)->AllocObject(env, global_class_cache.Language); 17 | (*env)->SetLongField(env, language_obj, global_field_cache.Language_self, (jlong)language); 18 | return language_obj; 19 | }; 20 | 21 | jshort JNICALL lookahead_iterator_get_current_symbol(JNIEnv *env, jobject this) { 22 | TSLookaheadIterator *self = GET_POINTER(TSLookaheadIterator, this, LookaheadIterator_self); 23 | return (jshort)ts_lookahead_iterator_current_symbol(self); 24 | } 25 | 26 | jstring JNICALL lookahead_iterator_get_current_symbol_name(JNIEnv *env, jobject this) { 27 | TSLookaheadIterator *self = GET_POINTER(TSLookaheadIterator, this, LookaheadIterator_self); 28 | const char *name = ts_lookahead_iterator_current_symbol_name(self); 29 | return (*env)->NewStringUTF(env, name); 30 | } 31 | 32 | jboolean JNICALL lookahead_iterator_reset(JNIEnv *env, jobject this, jshort state, 33 | jobject language) { 34 | TSLookaheadIterator *self = GET_POINTER(TSLookaheadIterator, this, LookaheadIterator_self); 35 | if (language == NULL) { 36 | return (jboolean)ts_lookahead_iterator_reset_state(self, (uint16_t)state); 37 | } 38 | TSLanguage *language_ptr = GET_POINTER(TSLanguage, language, Language_self); 39 | return (jboolean)ts_lookahead_iterator_reset(self, language_ptr, (uint16_t)state); 40 | } 41 | 42 | jboolean JNICALL lookahead_iterator_native_next(JNIEnv *env, jobject this) { 43 | TSLookaheadIterator *self = GET_POINTER(TSLookaheadIterator, this, LookaheadIterator_self); 44 | return (jboolean)ts_lookahead_iterator_next(self); 45 | } 46 | 47 | const JNINativeMethod LookaheadIterator_methods[] = { 48 | {"init", "(JS)J", (void *)&lookahead_iterator_init}, 49 | {"delete", "(J)V", (void *)&lookahead_iterator_delete}, 50 | {"getLanguage", "()L" PACKAGE "Language;", (void *)&lookahead_iterator_get_language}, 51 | {"getCurrentSymbol", "()S", (void *)&lookahead_iterator_get_current_symbol}, 52 | {"getCurrentSymbolName", "()Ljava/lang/String;", 53 | (void *)&lookahead_iterator_get_current_symbol_name}, 54 | {"reset", "(SL" PACKAGE "Language;)Z", (void *)&lookahead_iterator_reset}, 55 | {"nativeNext", "()Z", (void *)&lookahead_iterator_native_next}, 56 | }; 57 | 58 | const size_t LookaheadIterator_methods_size = 59 | sizeof LookaheadIterator_methods / sizeof(JNINativeMethod); 60 | -------------------------------------------------------------------------------- /ktreesitter/src/jni/tree.c: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | jlong JNICALL tree_copy CRITICAL_ARGS(jlong self) { return (jlong)ts_tree_copy((TSTree *)self); } 4 | 5 | void JNICALL tree_delete CRITICAL_ARGS(jlong self) { ts_tree_delete((TSTree *)self); } 6 | 7 | jobject JNICALL tree_get_root_node(JNIEnv *env, jobject this) { 8 | TSTree *self = GET_POINTER(TSTree, this, Tree_self); 9 | return marshal_node(env, ts_tree_root_node(self), this); 10 | } 11 | 12 | jobject JNICALL tree_root_node_with_offset(JNIEnv *env, jobject this, jint bytes, jobject extent) { 13 | TSTree *self = GET_POINTER(TSTree, this, Tree_self); 14 | TSPoint ts_point = unmarshal_point(env, extent); 15 | TSNode ts_node = ts_tree_root_node_with_offset(self, (uint32_t)bytes, ts_point); 16 | return marshal_node(env, ts_node, this); 17 | } 18 | 19 | void JNICALL tree_edit(JNIEnv *env, jobject this, jobject edit) { 20 | TSTree *self = GET_POINTER(TSTree, this, Tree_self); 21 | TSInputEdit input_edit = unmarshal_input_edit(env, edit); 22 | ts_tree_edit(self, &input_edit); 23 | (*env)->SetObjectField(env, this, global_field_cache.Tree_source, NULL); 24 | } 25 | 26 | jobject JNICALL tree_changed_ranges(JNIEnv *env, jobject this, jobject new_tree) { 27 | TSTree *self = GET_POINTER(TSTree, this, Tree_self); 28 | TSTree *other = GET_POINTER(TSTree, new_tree, Tree_self); 29 | uint32_t length; 30 | TSRange *ts_ranges = ts_tree_get_changed_ranges(self, other, &length); 31 | if (length == 0 || ts_ranges == NULL) 32 | return NEW_OBJECT(ArrayList, 0); 33 | 34 | TSRange *ts_range = ts_ranges; 35 | jobject ranges = NEW_OBJECT(ArrayList, (jint)length); 36 | for (uint32_t i = 0; i < length; ++i) { 37 | jobject range_obj = marshal_range(env, ts_range++); 38 | CALL_METHOD(Boolean, ranges, ArrayList_add, range_obj); 39 | (*env)->DeleteLocalRef(env, range_obj); 40 | } 41 | free(ts_ranges); 42 | return ranges; 43 | } 44 | 45 | jobject JNICALL tree_native_included_ranges(JNIEnv *env, jobject this) { 46 | TSTree *self = GET_POINTER(TSTree, this, Tree_self); 47 | uint32_t length; 48 | TSRange *ts_ranges = ts_tree_included_ranges(self, &length); 49 | if (length == 0 || ts_ranges == NULL) 50 | return NEW_OBJECT(ArrayList, 0); 51 | 52 | jobject ranges = NEW_OBJECT(ArrayList, (jint)length); 53 | for (uint32_t i = 0; i < length; ++i) { 54 | jobject range_obj = marshal_range(env, ts_ranges + i); 55 | CALL_METHOD(Boolean, ranges, ArrayList_add, range_obj); 56 | (*env)->DeleteLocalRef(env, range_obj); 57 | } 58 | free(ts_ranges); 59 | return ranges; 60 | } 61 | 62 | const JNINativeMethod Tree_methods[] = { 63 | {"copy", "(J)J", (void *)&tree_copy}, 64 | {"delete", "(J)V", (void *)&tree_delete}, 65 | {"getRootNode", "()L" PACKAGE "Node;", (void *)&tree_get_root_node}, 66 | {"rootNodeWithOffset", "(IL" PACKAGE "Point;)L" PACKAGE "Node;", 67 | (void *)&tree_root_node_with_offset}, 68 | {"edit", "(L" PACKAGE "InputEdit;)V", (void *)&tree_edit}, 69 | {"changedRanges", "(L" PACKAGE "Tree;)Ljava/util/List;", (void *)&tree_changed_ranges}, 70 | {"nativeIncludedRanges", "()Ljava/util/List;", (void *)&tree_native_included_ranges}, 71 | }; 72 | 73 | const size_t Tree_methods_size = sizeof Tree_methods / sizeof(JNINativeMethod); 74 | -------------------------------------------------------------------------------- /ktreesitter/src/jni/tree_cursor.c: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | #define SET_INTERNAL_NODE(value) \ 4 | (*env)->SetObjectField(env, this, global_field_cache.TreeCursor_internalNode, value); 5 | 6 | static inline TSTreeCursor *tree_cursor_alloc(TSTreeCursor cursor) { 7 | TSTreeCursor *cursor_ptr = (TSTreeCursor *)malloc(sizeof(TSTreeCursor)); 8 | cursor_ptr->id = cursor.id; 9 | cursor_ptr->tree = cursor.tree; 10 | cursor_ptr->context[0] = cursor.context[0]; 11 | cursor_ptr->context[1] = cursor.context[1]; 12 | cursor_ptr->context[2] = cursor.context[2]; 13 | return cursor_ptr; 14 | } 15 | 16 | jlong JNICALL tree_cursor_init(JNIEnv *env, jclass _class, jobject node) { 17 | TSNode ts_node = unmarshal_node(env, node); 18 | TSTreeCursor cursor = ts_tree_cursor_new(ts_node); 19 | return (jlong)tree_cursor_alloc(cursor); 20 | } 21 | 22 | jlong JNICALL tree_cursor_copy CRITICAL_ARGS(jlong self) { 23 | TSTreeCursor copy = ts_tree_cursor_copy((TSTreeCursor *)self); 24 | return (jlong)tree_cursor_alloc(copy); 25 | } 26 | 27 | void JNICALL tree_cursor_delete CRITICAL_ARGS(jlong self) { 28 | ts_tree_cursor_delete((TSTreeCursor *)self); 29 | free((TSTreeCursor *)self); 30 | } 31 | 32 | jobject JNICALL tree_cursor_get_current_node(JNIEnv *env, jobject this) { 33 | jobject node = GET_FIELD(Object, this, TreeCursor_internalNode); 34 | if (node != NULL) 35 | return node; 36 | 37 | TSTreeCursor *self = GET_POINTER(TSTreeCursor, this, TreeCursor_self); 38 | jobject tree = GET_FIELD(Object, this, TreeCursor_tree); 39 | TSNode ts_node = ts_tree_cursor_current_node(self); 40 | node = marshal_node(env, ts_node, tree); 41 | SET_INTERNAL_NODE(node); 42 | return node; 43 | } 44 | 45 | jint JNICALL tree_cursor_get_current_depth(JNIEnv *env, jobject this) { 46 | TSTreeCursor *self = GET_POINTER(TSTreeCursor, this, TreeCursor_self); 47 | return (jint)ts_tree_cursor_current_depth(self); 48 | } 49 | 50 | jshort JNICALL tree_cursor_get_current_field_id(JNIEnv *env, jobject this) { 51 | TSTreeCursor *self = GET_POINTER(TSTreeCursor, this, TreeCursor_self); 52 | return (short)ts_tree_cursor_current_field_id(self); 53 | } 54 | 55 | jstring JNICALL tree_cursor_get_current_field_name(JNIEnv *env, jobject this) { 56 | TSTreeCursor *self = GET_POINTER(TSTreeCursor, this, TreeCursor_self); 57 | const char *name = ts_tree_cursor_current_field_name(self); 58 | return name ? (*env)->NewStringUTF(env, name) : NULL; 59 | } 60 | 61 | jint JNICALL tree_cursor_get_current_descendant_index(JNIEnv *env, jobject this) { 62 | TSTreeCursor *self = GET_POINTER(TSTreeCursor, this, TreeCursor_self); 63 | return (jint)ts_tree_cursor_current_descendant_index(self); 64 | } 65 | 66 | void JNICALL tree_cursor_reset__node(JNIEnv *env, jobject this, jobject node) { 67 | TSTreeCursor *self = GET_POINTER(TSTreeCursor, this, TreeCursor_self); 68 | TSNode ts_node = unmarshal_node(env, node); 69 | ts_tree_cursor_reset(self, ts_node); 70 | SET_INTERNAL_NODE(NULL); 71 | } 72 | 73 | void JNICALL tree_cursor_reset__cursor(JNIEnv *env, jobject this, jobject cursor) { 74 | TSTreeCursor *self = GET_POINTER(TSTreeCursor, this, TreeCursor_self); 75 | TSTreeCursor *other = GET_POINTER(TSTreeCursor, cursor, TreeCursor_self); 76 | ts_tree_cursor_reset_to(self, other); 77 | SET_INTERNAL_NODE(NULL); 78 | } 79 | 80 | jboolean JNICALL tree_cursor_goto_first_child(JNIEnv *env, jobject this) { 81 | TSTreeCursor *self = GET_POINTER(TSTreeCursor, this, TreeCursor_self); 82 | bool result = ts_tree_cursor_goto_first_child(self); 83 | if (result) 84 | SET_INTERNAL_NODE(NULL); 85 | return (jboolean)result; 86 | } 87 | 88 | jboolean JNICALL tree_cursor_goto_last_child(JNIEnv *env, jobject this) { 89 | TSTreeCursor *self = GET_POINTER(TSTreeCursor, this, TreeCursor_self); 90 | bool result = ts_tree_cursor_goto_last_child(self); 91 | if (result) 92 | SET_INTERNAL_NODE(NULL); 93 | return (jboolean)result; 94 | } 95 | 96 | jboolean JNICALL tree_cursor_goto_parent(JNIEnv *env, jobject this) { 97 | TSTreeCursor *self = GET_POINTER(TSTreeCursor, this, TreeCursor_self); 98 | bool result = ts_tree_cursor_goto_parent(self); 99 | if (result) 100 | SET_INTERNAL_NODE(NULL); 101 | return (jboolean)result; 102 | } 103 | 104 | jboolean JNICALL tree_cursor_goto_next_sibling(JNIEnv *env, jobject this) { 105 | TSTreeCursor *self = GET_POINTER(TSTreeCursor, this, TreeCursor_self); 106 | bool result = ts_tree_cursor_goto_next_sibling(self); 107 | if (result) 108 | SET_INTERNAL_NODE(NULL); 109 | return (jboolean)result; 110 | } 111 | 112 | jboolean JNICALL tree_cursor_goto_previous_sibling(JNIEnv *env, jobject this) { 113 | TSTreeCursor *self = GET_POINTER(TSTreeCursor, this, TreeCursor_self); 114 | bool result = ts_tree_cursor_goto_previous_sibling(self); 115 | if (result) 116 | SET_INTERNAL_NODE(NULL); 117 | return (jboolean)result; 118 | } 119 | 120 | void JNICALL tree_cursor_goto_descendant(JNIEnv *env, jobject this, jint index) { 121 | TSTreeCursor *self = GET_POINTER(TSTreeCursor, this, TreeCursor_self); 122 | ts_tree_cursor_goto_descendant(self, (uint32_t)index); 123 | SET_INTERNAL_NODE(NULL); 124 | } 125 | 126 | jlong JNICALL tree_cursor_native_goto_first_child_for_byte(JNIEnv *env, jobject this, jint byte) { 127 | TSTreeCursor *self = GET_POINTER(TSTreeCursor, this, TreeCursor_self); 128 | return (jlong)ts_tree_cursor_goto_first_child_for_byte(self, (uint32_t)byte); 129 | } 130 | 131 | jlong JNICALL tree_cursor_native_goto_first_child_for_point(JNIEnv *env, jobject this, 132 | jobject point) { 133 | TSTreeCursor *self = GET_POINTER(TSTreeCursor, this, TreeCursor_self); 134 | TSPoint ts_point = unmarshal_point(env, point); 135 | return (jlong)ts_tree_cursor_goto_first_child_for_point(self, ts_point); 136 | } 137 | 138 | const JNINativeMethod TreeCursor_methods[] = { 139 | {"init", "(L" PACKAGE "Node;)J", (void *)&tree_cursor_init}, 140 | {"copy", "(J)J", (void *)&tree_cursor_copy}, 141 | {"delete", "(J)V", (void *)&tree_cursor_delete}, 142 | {"getCurrentNode", "()L" PACKAGE "Node;", (void *)&tree_cursor_get_current_node}, 143 | {"getCurrentDepth", "()I", (void *)&tree_cursor_get_current_depth}, 144 | {"getCurrentFieldId", "()S", (void *)&tree_cursor_get_current_field_id}, 145 | {"getCurrentFieldName", "()Ljava/lang/String;", (void *)&tree_cursor_get_current_field_name}, 146 | {"getCurrentDescendantIndex", "()I", (void *)&tree_cursor_get_current_descendant_index}, 147 | {"reset", "(L" PACKAGE "Node;)V", (void *)&tree_cursor_reset__node}, 148 | {"reset", "(L" PACKAGE "TreeCursor;)V", (void *)&tree_cursor_reset__cursor}, 149 | {"gotoFirstChild", "()Z", (void *)&tree_cursor_goto_first_child}, 150 | {"gotoLastChild", "()Z", (void *)&tree_cursor_goto_last_child}, 151 | {"gotoParent", "()Z", (void *)&tree_cursor_goto_parent}, 152 | {"gotoNextSibling", "()Z", (void *)&tree_cursor_goto_next_sibling}, 153 | {"gotoPreviousSibling", "()Z", (void *)&tree_cursor_goto_previous_sibling}, 154 | {"gotoDescendant", "(I)V", (void *)&tree_cursor_goto_descendant}, 155 | {"nativeGotoFirstChildForByte", "(I)J", (void *)&tree_cursor_native_goto_first_child_for_byte}, 156 | {"nativeGotoFirstChildForPoint", "(L" PACKAGE "Point;)J", 157 | (void *)&tree_cursor_native_goto_first_child_for_point}, 158 | }; 159 | 160 | const size_t TreeCursor_methods_size = sizeof TreeCursor_methods / sizeof(JNINativeMethod); 161 | -------------------------------------------------------------------------------- /ktreesitter/src/jvmMain/kotlin/io/github/treesitter/ktreesitter/Language.kt: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter 2 | 3 | /** 4 | * A class that defines how to parse a particular language. 5 | * 6 | * When a [Language] is generated by the Tree-sitter CLI, it is assigned 7 | * an ABI [version] number that corresponds to the current CLI version. 8 | * 9 | * @constructor Create a new instance from the given language pointer. 10 | * @param language A pointer to a `TSLanguage` cast to [Long]. 11 | * @throws [IllegalArgumentException] If the pointer is invalid or the [version] is incompatible. 12 | */ 13 | actual class Language @Throws(IllegalArgumentException::class) actual constructor(language: Any) { 14 | @JvmField 15 | internal val self: Long = (language as? Long)?.takeIf { it > 0L } 16 | ?: throw IllegalArgumentException("Invalid language: $language") 17 | 18 | init { 19 | checkVersion() 20 | } 21 | 22 | /** The ABI version number for this language. */ 23 | @get:JvmName("getVersion") 24 | actual val version: UInt 25 | external get 26 | 27 | /** The number of distinct node types in this language. */ 28 | @get:JvmName("getSymbolCount") 29 | actual val symbolCount: UInt 30 | external get 31 | 32 | /** The number of valid states in this language. */ 33 | @get:JvmName("getStateCount") 34 | actual val stateCount: UInt 35 | external get 36 | 37 | /** The number of distinct field names in this language. */ 38 | @get:JvmName("getFieldCount") 39 | actual val fieldCount: UInt 40 | external get 41 | 42 | /** 43 | * Get another reference to the language. 44 | * 45 | * @since 0.24.0 46 | */ 47 | actual fun copy() = Language(copy(self)) 48 | 49 | /** Get the node type for the given numerical ID. */ 50 | @JvmName("symbolName") 51 | actual external fun symbolName(symbol: UShort): String? 52 | 53 | /** Get the numerical ID for the given node type. */ 54 | @JvmName("symbolForName") 55 | actual external fun symbolForName(name: String, isNamed: Boolean): UShort 56 | 57 | /** 58 | * Check if the node for the given numerical ID is named 59 | * 60 | * @see [Node.isNamed] 61 | */ 62 | @JvmName("isNamed") 63 | actual external fun isNamed(symbol: UShort): Boolean 64 | 65 | /** Check if the node for the given numerical ID is visible. */ 66 | @JvmName("isVisible") 67 | actual external fun isVisible(symbol: UShort): Boolean 68 | 69 | /** 70 | * Check if the node for the given numerical ID is a supertype. 71 | * 72 | * @since 0.24.0 73 | */ 74 | @JvmName("isSupertype") 75 | actual external fun isSupertype(symbol: UShort): Boolean 76 | 77 | /** Get the field name for the given numerical id. */ 78 | @JvmName("fieldNameForId") 79 | actual external fun fieldNameForId(id: UShort): String? 80 | 81 | /** Get the numerical ID for the given field name. */ 82 | @JvmName("fieldIdForName") 83 | actual external fun fieldIdForName(name: String): UShort 84 | 85 | /** 86 | * Get the next parse state. 87 | * 88 | * Combine this with [lookaheadIterator] to generate 89 | * completion suggestions or valid symbols in error nodes. 90 | * 91 | * #### Example 92 | * 93 | * ```kotlin 94 | * language.nextState(node.parseState, node.grammarSymbol) 95 | * ``` 96 | */ 97 | @JvmName("nextState") 98 | actual external fun nextState(state: UShort, symbol: UShort): UShort 99 | 100 | /** 101 | * Create a new [lookahead iterator][LookaheadIterator] for the given parse state. 102 | * 103 | * @throws [IllegalArgumentException] If the state is invalid for this language. 104 | */ 105 | @JvmName("lookaheadIterator") 106 | @Throws(IllegalArgumentException::class) 107 | actual fun lookaheadIterator(state: UShort) = LookaheadIterator(this, state) 108 | 109 | /** 110 | * Create a new [Query] from a string containing one or more S-expression 111 | * [patterns](https://tree-sitter.github.io/tree-sitter/using-parsers#query-syntax). 112 | * 113 | * @throws [QueryError] If any error occurred while creating the query. 114 | */ 115 | @Throws(QueryError::class) 116 | actual fun query(source: String) = Query(this, source) 117 | 118 | actual override fun equals(other: Any?) = 119 | this === other || (other is Language && self == other.self) 120 | 121 | actual override fun hashCode() = self.hashCode() 122 | 123 | override fun toString() = "Language(id=0x${self.toString(16)}, version=$version)" 124 | 125 | @Throws(IllegalArgumentException::class) 126 | private external fun checkVersion() 127 | 128 | private companion object { 129 | @JvmStatic 130 | private external fun copy(self: Long): Long 131 | 132 | init { 133 | NativeUtils.loadLibrary() 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /ktreesitter/src/jvmMain/kotlin/io/github/treesitter/ktreesitter/LookaheadIterator.kt: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter 2 | 3 | /** 4 | * A class that is used to look up valid symbols in a specific parse state. 5 | * 6 | * Lookahead iterators can be useful to generate suggestions and improve syntax 7 | * error diagnostics. To get symbols valid in an `ERROR` node, use the lookahead 8 | * iterator on its first leaf node state. For `MISSING` nodes, a lookahead 9 | * iterator created on the previous non-extra leaf node may be appropriate. 10 | */ 11 | actual class LookaheadIterator @Throws(IllegalArgumentException::class) internal constructor( 12 | language: Language, 13 | private val state: UShort 14 | ) : AbstractIterator() { 15 | private val self: Long = init(language.self, state).takeIf { it > 0L } 16 | ?: throw IllegalArgumentException("State $state is not valid for $language") 17 | 18 | init { 19 | RefCleaner(this, CleanAction(self)) 20 | } 21 | 22 | /** The current language of the lookahead iterator. */ 23 | actual val language: Language 24 | external get 25 | 26 | /** 27 | * The current symbol ID. 28 | * 29 | * The ID of the `ERROR` symbol is equal to `UShort.MAX_VALUE`. 30 | */ 31 | @get:JvmName("getCurrentSymbol") 32 | actual val currentSymbol: UShort 33 | external get 34 | 35 | /** 36 | * The current symbol name. 37 | * 38 | * Newly created lookahead iterators will contain the `ERROR` symbol. 39 | */ 40 | actual val currentSymbolName: String 41 | external get 42 | 43 | /** 44 | * Reset the lookahead iterator the given [state] and, optionally, another [language]. 45 | * 46 | * @return `true` if the iterator was reset successfully or `false` if it failed. 47 | */ 48 | @JvmName("reset") 49 | actual external fun reset(state: UShort, language: Language?): Boolean 50 | 51 | /** Advance the lookahead iterator to the next symbol. */ 52 | actual override fun next() = super.next() 53 | 54 | /** Iterate over the symbol IDs. */ 55 | actual fun symbols(): Sequence { 56 | reset(state) 57 | return sequence { 58 | while (nativeNext()) { 59 | yield(currentSymbol) 60 | } 61 | } 62 | } 63 | 64 | /** Iterate over the symbol names. */ 65 | actual fun symbolNames(): Sequence { 66 | reset(state) 67 | return sequence { 68 | while (nativeNext()) { 69 | yield(currentSymbolName) 70 | } 71 | } 72 | } 73 | 74 | override fun computeNext() = if (nativeNext()) { 75 | setNext(Symbol(currentSymbol, currentSymbolName)) 76 | } else { 77 | done() 78 | } 79 | 80 | operator fun iterator() = apply { reset(state) } 81 | 82 | private external fun nativeNext(): Boolean 83 | 84 | /** A class that pairs a symbol ID with its name. */ 85 | @JvmRecord 86 | actual data class Symbol actual constructor(actual val id: UShort, actual val name: String) 87 | 88 | private class CleanAction(private val ptr: Long) : Runnable { 89 | override fun run() = delete(ptr) 90 | } 91 | 92 | private companion object { 93 | @JvmStatic 94 | @JvmName("init") 95 | private external fun init(language: Long, state: UShort): Long 96 | 97 | @JvmStatic 98 | private external fun delete(self: Long) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /ktreesitter/src/jvmMain/kotlin/io/github/treesitter/ktreesitter/NativeUtils.kt: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter 2 | 3 | import java.io.File.createTempFile 4 | 5 | internal object NativeUtils { 6 | private const val LIB_NAME = "ktreesitter" 7 | 8 | @JvmStatic 9 | @Throws(UnsupportedOperationException::class) 10 | private fun libPath(): String? { 11 | val osName = System.getProperty("os.name")!!.lowercase() 12 | val archName = System.getProperty("os.arch")!!.lowercase() 13 | val ext: String 14 | val os: String 15 | val prefix: String 16 | when { 17 | "windows" in osName -> { 18 | ext = "dll" 19 | os = "windows" 20 | prefix = "" 21 | } 22 | "linux" in osName -> { 23 | ext = "so" 24 | os = "linux" 25 | prefix = "lib" 26 | } 27 | "mac" in osName -> { 28 | ext = "dylib" 29 | os = "macos" 30 | prefix = "lib" 31 | } 32 | else -> { 33 | throw UnsupportedOperationException("Unsupported operating system: $osName") 34 | } 35 | } 36 | val arch = when { 37 | "amd64" in archName || "x86_64" in archName -> "x64" 38 | "aarch64" in archName || "arm64" in archName -> "aarch64" 39 | else -> throw UnsupportedOperationException("Unsupported architecture: $archName") 40 | } 41 | val libUrl = javaClass.getResource("/lib/$os/$arch/$prefix$LIB_NAME.$ext") ?: return null 42 | return createTempFile("$prefix$LIB_NAME", ".$ext").apply { 43 | writeBytes(libUrl.openStream().use { it.readAllBytes() }) 44 | deleteOnExit() 45 | }.path 46 | } 47 | 48 | @JvmStatic 49 | @Throws(UnsatisfiedLinkError::class) 50 | internal fun loadLibrary() { 51 | try { 52 | System.loadLibrary(LIB_NAME) 53 | } catch (ex: UnsatisfiedLinkError) { 54 | @Suppress("UnsafeDynamicallyLoadedCode") 55 | System.load(libPath() ?: throw ex) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /ktreesitter/src/jvmMain/kotlin/io/github/treesitter/ktreesitter/Parser.kt: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter 2 | 3 | /** 4 | * A class that is used to produce a [syntax tree][Tree] from source code. 5 | * 6 | * @constructor Create a new instance with a certain [language], or `null` if empty. 7 | */ 8 | actual class Parser actual constructor() { 9 | actual constructor(language: Language) : this() { 10 | this.language = language 11 | } 12 | 13 | private val self = init() 14 | 15 | init { 16 | RefCleaner(this, CleanAction(self)) 17 | } 18 | 19 | /** 20 | * The language that the parser will use for parsing. 21 | * 22 | * Parsing cannot be performed while the language is `null`. 23 | */ 24 | actual var language: Language? = null 25 | external set 26 | 27 | /** 28 | * The ranges of text that the parser will include when parsing. 29 | * 30 | * By default, the parser will always include entire documents. 31 | * Setting this property allows you to parse only a _portion_ of a 32 | * document but still return a syntax tree whose ranges match up with 33 | * the document as a whole. You can also pass multiple disjoint ranges. 34 | * 35 | * @throws [IllegalArgumentException] If the ranges overlap or are not in ascending order. 36 | */ 37 | @set:Throws(IllegalArgumentException::class) 38 | actual var includedRanges: List = emptyList() 39 | external set 40 | 41 | /** 42 | * The maximum duration in microseconds that parsing 43 | * should be allowed to take before halting. 44 | */ 45 | @get:JvmName("getTimeoutMicros") 46 | @set:JvmName("setTimeoutMicros") 47 | actual var timeoutMicros: ULong 48 | external get 49 | external set 50 | 51 | /** 52 | * The logger that the parser will use during parsing. 53 | * 54 | * #### Example 55 | * 56 | * ``` 57 | * import org.slf4j.LoggerFactory; 58 | * import org.slf4j.MarkerFactory; 59 | * 60 | * val logger = LoggerFactory.getLogger(parser.javaClass) 61 | * val lexMarker = MarkerFactory.getMarker("TS LEX") 62 | * val parseMarker = MarkerFactory.getMarker("TS PARSE") 63 | * 64 | * parser.logger = { type, msg -> 65 | * val marker = when (type) { 66 | * LogType.LEX -> lexMarker 67 | * LogType.PARSE -> parseMarker 68 | * } 69 | * logger.debug(marker, msg) 70 | * } 71 | * ``` 72 | */ 73 | @get:Deprecated("Don't call the logger directly.", level = DeprecationLevel.HIDDEN) 74 | actual var logger: LogFunction? = null 75 | external set 76 | 77 | /** 78 | * Parse a source code string and create a syntax tree. 79 | * 80 | * If you have already parsed an earlier version of this document and the document 81 | * has since been edited, pass the previous syntax tree to [oldTree] so that the 82 | * unchanged parts of it can be reused. This will save time and memory. For this 83 | * to work correctly, you must have already edited the old syntax tree using the 84 | * [Tree.edit] method in a way that exactly matches the source code changes. 85 | * 86 | * @throws [IllegalStateException] 87 | * If the parser does not have a [language] assigned or 88 | * if parsing was cancelled due to a [timeout][timeoutMicros]. 89 | */ 90 | @Throws(IllegalStateException::class) 91 | actual external fun parse(source: String, oldTree: Tree?): Tree 92 | 93 | /** 94 | * Parse source code from a callback and create a syntax tree. 95 | * 96 | * If you have already parsed an earlier version of this document and the document 97 | * has since been edited, pass the previous syntax tree to [oldTree] so that the 98 | * unchanged parts of it can be reused. This will save time and memory. For this 99 | * to work correctly, you must have already edited the old syntax tree using the 100 | * [Tree.edit] method in a way that exactly matches the source code changes. 101 | * 102 | * @throws [IllegalStateException] 103 | * If the parser does not have a [language] assigned or 104 | * if parsing was cancelled due to a [timeout][timeoutMicros]. 105 | */ 106 | @Throws(IllegalStateException::class) 107 | actual external fun parse(oldTree: Tree?, callback: ParseCallback): Tree 108 | 109 | /** 110 | * Instruct the parser to start the next [parse] from the beginning. 111 | * 112 | * If the parser previously failed because of a [timeout][timeoutMicros], 113 | * then by default, it will resume where it left off. If you don't 114 | * want to resume, and instead intend to use this parser to parse 115 | * some other document, you must call this method first. 116 | */ 117 | actual external fun reset() 118 | 119 | override fun toString() = "Parser(language=$language)" 120 | 121 | /** The type of a log message. */ 122 | @Suppress("unused") 123 | actual enum class LogType { LEX, PARSE } 124 | 125 | private class CleanAction(private val ptr: Long) : Runnable { 126 | override fun run() = delete(ptr) 127 | } 128 | 129 | private companion object { 130 | @JvmStatic 131 | private external fun init(): Long 132 | 133 | @JvmStatic 134 | private external fun delete(self: Long) 135 | 136 | init { 137 | NativeUtils.loadLibrary() 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /ktreesitter/src/jvmMain/kotlin/io/github/treesitter/ktreesitter/RefCleaner.kt: -------------------------------------------------------------------------------- 1 | @file:JvmName("RefCleaner") 2 | 3 | package io.github.treesitter.ktreesitter 4 | 5 | import java.lang.ref.Cleaner 6 | 7 | internal object RefCleaner { 8 | private val INSTANCE: Cleaner = Cleaner.create() 9 | 10 | @JvmName("register") 11 | operator fun invoke(obj: Any, action: Runnable) { 12 | INSTANCE.register(obj, action) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ktreesitter/src/jvmMain/kotlin/io/github/treesitter/ktreesitter/Tree.kt: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter 2 | 3 | /** A class that represents a syntax tree. */ 4 | @Suppress("CanBeParameter") 5 | actual class Tree internal constructor( 6 | private val self: Long, 7 | private var source: String?, 8 | /** The language that was used to parse the syntax tree. */ 9 | actual val language: Language 10 | ) { 11 | init { 12 | RefCleaner(this, CleanAction(self)) 13 | } 14 | 15 | /** The root node of the syntax tree. */ 16 | actual val rootNode: Node 17 | external get 18 | 19 | /** The included ranges that were used to parse the syntax tree. */ 20 | actual val includedRanges by lazy { nativeIncludedRanges() } 21 | 22 | /** 23 | * Get the root node of the syntax tree, but with 24 | * its position shifted forward by the given offset. 25 | */ 26 | @JvmName("rootNodeWithOffset") 27 | actual external fun rootNodeWithOffset(bytes: UInt, extent: Point): Node? 28 | 29 | /** 30 | * Edit the syntax tree to keep it in sync 31 | * with source code that has been modified. 32 | */ 33 | actual external fun edit(edit: InputEdit) 34 | 35 | /** 36 | * Create a shallow copy of the syntax tree. 37 | * 38 | * You need to copy a syntax tree in order to use it on multiple 39 | * threads or coroutines, as syntax trees are not thread safe. 40 | */ 41 | actual fun copy() = Tree(copy(self), source, language) 42 | 43 | /** Create a new tree cursor starting from the node of the tree. */ 44 | actual fun walk() = TreeCursor(rootNode) 45 | 46 | /** Get the source code of the syntax tree, if available. */ 47 | actual fun text(): CharSequence? = source 48 | 49 | /** 50 | * Compare an old edited syntax tree to a new 51 | * syntax tree representing the same document. 52 | * 53 | * For this to work correctly, this tree must have been 54 | * edited such that its ranges match up to the new tree. 55 | * 56 | * @return A list of ranges whose syntactic structure has changed. 57 | */ 58 | actual external fun changedRanges(newTree: Tree): List 59 | 60 | override fun toString() = "Tree(language=$language, source=$source)" 61 | 62 | private external fun nativeIncludedRanges(): List 63 | 64 | private class CleanAction(private val ptr: Long) : Runnable { 65 | override fun run() = delete(ptr) 66 | } 67 | 68 | private companion object { 69 | @JvmStatic 70 | private external fun copy(self: Long): Long 71 | 72 | @JvmStatic 73 | private external fun delete(self: Long) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /ktreesitter/src/jvmMain/kotlin/io/github/treesitter/ktreesitter/TreeCursor.kt: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter 2 | 3 | /** A class that can be used to efficiently walk a [syntax tree][Tree]. */ 4 | actual class TreeCursor private constructor( 5 | private val self: Long, 6 | @JvmField internal actual val tree: Tree 7 | ) { 8 | internal constructor(node: Node) : this(init(node), node.tree) { 9 | internalNode = node 10 | } 11 | 12 | init { 13 | RefCleaner(this, CleanAction(self)) 14 | } 15 | 16 | @Suppress("unused") 17 | private var internalNode: Node? = null 18 | 19 | /** The current node of the cursor. */ 20 | actual val currentNode: Node 21 | external get 22 | 23 | /** 24 | * The depth of the cursor's current node relative to the 25 | * original node that the cursor was constructed with. 26 | */ 27 | @get:JvmName("getCurrentDepth") 28 | actual val currentDepth: UInt 29 | external get 30 | 31 | /** 32 | * The field ID of the tree cursor's current node, or `0`. 33 | * 34 | * @see [Node.childByFieldId] 35 | * @see [Language.fieldIdForName] 36 | */ 37 | @get:JvmName("getCurrentFieldId") 38 | actual val currentFieldId: UShort 39 | external get 40 | 41 | /** 42 | * The field name of the tree cursor's current node, if available. 43 | * 44 | * @see [Node.childByFieldName] 45 | */ 46 | actual val currentFieldName: String? 47 | external get 48 | 49 | /** 50 | * The index of the cursor's current node out of all the descendants 51 | * of the original node that the cursor was constructed with. 52 | */ 53 | @get:JvmName("getCurrentDescendantIndex") 54 | actual val currentDescendantIndex: UInt 55 | external get 56 | 57 | /** Create a shallow copy of the tree cursor. */ 58 | actual fun copy() = TreeCursor(copy(self), tree) 59 | 60 | /** Reset the cursor to start at a different node. */ 61 | actual external fun reset(node: Node) 62 | 63 | /** Reset the cursor to start at the same position as another cursor. */ 64 | actual external fun reset(cursor: TreeCursor) 65 | 66 | /** 67 | * Move the cursor to the first child of its current node. 68 | * 69 | * @return 70 | * `true` if the cursor successfully moved, 71 | * or `false` if there were no children. 72 | */ 73 | actual external fun gotoFirstChild(): Boolean 74 | 75 | /** 76 | * Move the cursor to the last child of its current node. 77 | * 78 | * @return 79 | * `true` if the cursor successfully moved, 80 | * or `false` if there were no children. 81 | */ 82 | actual external fun gotoLastChild(): Boolean 83 | 84 | /** 85 | * Move the cursor to the parent of its current node. 86 | * 87 | * @return 88 | * `true` if the cursor successfully moved, 89 | * or `false` if there was no parent node. 90 | */ 91 | actual external fun gotoParent(): Boolean 92 | 93 | /** 94 | * Move the cursor to the next sibling of its current node. 95 | * 96 | * @return 97 | * `true` if the cursor successfully moved, 98 | * or `false` if there was no next sibling node. 99 | */ 100 | actual external fun gotoNextSibling(): Boolean 101 | 102 | /** 103 | * Move the cursor to the previous sibling of its current node. 104 | * 105 | * This function may be slower than [gotoNextSibling] due to how node positions 106 | * are stored. In the worst case, this will need to iterate through all the 107 | * children up to the previous sibling node to recalculate its position. 108 | * 109 | * @return 110 | * `true` if the cursor successfully moved, 111 | * or `false` if there was no previous sibling node. 112 | */ 113 | actual external fun gotoPreviousSibling(): Boolean 114 | 115 | /** 116 | * Move the cursor to the node that is the nth descendant of 117 | * the original node that the cursor was constructed with, 118 | * where `0` represents the original node itself. 119 | */ 120 | @JvmName("gotoDescendant") 121 | actual external fun gotoDescendant(index: UInt) 122 | 123 | /** 124 | * Move the cursor to the first child of its current 125 | * node that extends beyond the given byte offset. 126 | * 127 | * @return The index of the child node, or `null` if no such child was found. 128 | */ 129 | @JvmName("gotoFirstChildForByte") 130 | actual fun gotoFirstChildForByte(byte: UInt): UInt? { 131 | val result = nativeGotoFirstChildForByte(byte) 132 | if (result == -1L) return null 133 | internalNode = null 134 | return result.toUInt() 135 | } 136 | 137 | /** 138 | * Move the cursor to the first child of its current 139 | * node that extends beyond the given point offset. 140 | * 141 | * @return The index of the child node, or `null` if no such child was found. 142 | */ 143 | @JvmName("gotoFirstChildForPoint") 144 | actual fun gotoFirstChildForPoint(point: Point): UInt? { 145 | val result = nativeGotoFirstChildForPoint(point) 146 | if (result == -1L) return null 147 | internalNode = null 148 | return result.toUInt() 149 | } 150 | 151 | override fun toString() = "TreeCursor(tree=$tree)" 152 | 153 | @JvmName("nativeGotoFirstChildForByte") 154 | private external fun nativeGotoFirstChildForByte(byte: UInt): Long 155 | 156 | @JvmName("nativeGotoFirstChildForPoint") 157 | private external fun nativeGotoFirstChildForPoint(point: Point): Long 158 | 159 | private class CleanAction(private val ptr: Long) : Runnable { 160 | override fun run() = delete(ptr) 161 | } 162 | 163 | private companion object { 164 | @JvmStatic 165 | private external fun init(node: Node): Long 166 | 167 | @JvmStatic 168 | private external fun copy(self: Long): Long 169 | 170 | @JvmStatic 171 | private external fun delete(self: Long) 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /ktreesitter/src/nativeInterop/cinterop/treesitter.def: -------------------------------------------------------------------------------- 1 | package = io.github.treesitter.ktreesitter.internal 2 | headers = tree_sitter/api.h alloc.h 3 | headerFilter = tree_sitter/api.h 4 | compilerOpts = -DTREE_SITTER_HIDE_SYMBOLS 5 | staticLibraries = libtree-sitter.a 6 | strictEnums = \ 7 | TSLogType \ 8 | TSQuantifier \ 9 | TSQueryError 10 | nonStrictEnums = \ 11 | TSInputEncoding \ 12 | TSQueryPredicateStepType \ 13 | TSSymbolType 14 | excludedFunctions = \ 15 | ts_language_is_wasm \ 16 | ts_parser_set_wasm_store \ 17 | ts_parser_take_wasm_store \ 18 | ts_set_allocator \ 19 | ts_wasm_store_delete \ 20 | ts_wasm_store_language_count \ 21 | ts_wasm_store_load_language \ 22 | ts_wasm_store_new 23 | 24 | --- 25 | 26 | #define _isalnum(ch) \ 27 | ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9')) 28 | 29 | static inline bool kts_is_valid_identifier_char(int ch) { 30 | return _isalnum(ch) || ch == '_'; 31 | } 32 | 33 | static inline bool kts_is_valid_predicate_char(int ch) { 34 | return _isalnum(ch) || ch == '_' || ch == '-' || ch == '.' || ch == '?' || ch == '!'; 35 | } 36 | 37 | static inline uintptr_t kts_node_id(TSNode node) { 38 | return (uintptr_t)node.id; 39 | } 40 | 41 | static inline int32_t kts_node_hash(TSNode node) { 42 | uintptr_t id = (uintptr_t)node.id; 43 | uintptr_t tree = (uintptr_t)node.tree; 44 | return (int32_t)(id == tree ? id : id ^ tree); 45 | } 46 | 47 | static inline void *kts_malloc(size_t size) { 48 | return ts_malloc(size); 49 | } 50 | 51 | static inline void kts_free(void *ptr) { 52 | return ts_free(ptr); 53 | } 54 | -------------------------------------------------------------------------------- /ktreesitter/src/nativeMain/kotlin/io/github/treesitter/ktreesitter/Language.kt: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter 2 | 3 | import cnames.structs.TSLanguage 4 | import io.github.treesitter.ktreesitter.internal.* 5 | import kotlinx.cinterop.* 6 | 7 | /** 8 | * A class that defines how to parse a particular language. 9 | * 10 | * When a [Language] is generated by the Tree-sitter CLI, it is assigned 11 | * an ABI [version] number that corresponds to the current CLI version. 12 | */ 13 | @OptIn(ExperimentalForeignApi::class) actual class Language internal constructor( 14 | internal val self: CPointer 15 | ) { 16 | /** 17 | * Create a new instance from the given language pointer. 18 | * 19 | * @param language A [CPointer] to a `TSLanguage`. 20 | * @throws [IllegalArgumentException] 21 | * If the pointer is invalid or the [version] is incompatible. 22 | */ 23 | @Throws(IllegalArgumentException::class) 24 | actual constructor(language: Any) : this( 25 | (language as? CPointer<*>)?.rawValue?.let(::interpretCPointer) 26 | ?: throw IllegalArgumentException("Invalid language: $language") 27 | ) 28 | 29 | /** The ABI version number for this language. */ 30 | actual val version: UInt = ts_language_version(self) 31 | 32 | init { 33 | require(version in MIN_COMPATIBLE_LANGUAGE_VERSION..LANGUAGE_VERSION) { 34 | "Incompatible language version $version. " + 35 | "Must be between $MIN_COMPATIBLE_LANGUAGE_VERSION and $LANGUAGE_VERSION." 36 | } 37 | } 38 | 39 | /** The number of distinct node types in this language. */ 40 | actual val symbolCount: UInt = ts_language_symbol_count(self) 41 | 42 | /** The number of valid states in this language. */ 43 | actual val stateCount: UInt = ts_language_state_count(self) 44 | 45 | /** The number of distinct field names in this language. */ 46 | actual val fieldCount: UInt = ts_language_field_count(self) 47 | 48 | /** 49 | * Get another reference to the language. 50 | * 51 | * @since 0.24.0 52 | */ 53 | actual fun copy() = Language(ts_language_copy(self)!!) 54 | 55 | /** Get the node type for the given numerical ID. */ 56 | actual fun symbolName(symbol: UShort) = ts_language_symbol_name(self, symbol)?.toKString() 57 | 58 | /** Get the numerical ID for the given node type. */ 59 | actual fun symbolForName(name: String, isNamed: Boolean): UShort = 60 | ts_language_symbol_for_name(self, name, name.length.convert(), isNamed) 61 | 62 | /** 63 | * Check if the node for the given numerical ID is named 64 | * 65 | * @see [Node.isNamed] 66 | */ 67 | actual fun isNamed(symbol: UShort) = 68 | ts_language_symbol_type(self, symbol) == TSSymbolTypeRegular 69 | 70 | /** Check if the node for the given numerical ID is visible. */ 71 | actual fun isVisible(symbol: UShort) = 72 | ts_language_symbol_type(self, symbol) <= TSSymbolTypeAnonymous 73 | 74 | /** 75 | * Check if the node for the given numerical ID is a supertype. 76 | * 77 | * @since 0.24.0 78 | */ 79 | actual fun isSupertype(symbol: UShort) = 80 | ts_language_symbol_type(self, symbol) == TSSymbolTypeSupertype 81 | 82 | /** Get the field name for the given numerical id. */ 83 | actual fun fieldNameForId(id: UShort) = ts_language_field_name_for_id(self, id)?.toKString() 84 | 85 | /** Get the numerical ID for the given field name. */ 86 | actual fun fieldIdForName(name: String): UShort = 87 | ts_language_field_id_for_name(self, name, name.length.convert()) 88 | 89 | /** 90 | * Get the next parse state. 91 | * 92 | * Combine this with [lookaheadIterator] to generate 93 | * completion suggestions or valid symbols in error nodes. 94 | * 95 | * #### Example 96 | * 97 | * ```kotlin 98 | * language.nextState(node.parseState, node.grammarSymbol) 99 | * ``` 100 | */ 101 | actual fun nextState(state: UShort, symbol: UShort): UShort = 102 | ts_language_next_state(self, state, symbol) 103 | 104 | /** 105 | * Create a new [lookahead iterator][LookaheadIterator] for the given parse state. 106 | * 107 | * @throws [IllegalArgumentException] If the state is invalid for this language. 108 | */ 109 | @Throws( 110 | IllegalArgumentException::class 111 | ) actual fun lookaheadIterator(state: UShort) = LookaheadIterator(this, state) 112 | 113 | /** 114 | * Create a new [Query] from a string containing one or more S-expression 115 | * [patterns](https://tree-sitter.github.io/tree-sitter/using-parsers#query-syntax). 116 | * 117 | * @throws [QueryError] If any error occurred while creating the query. 118 | */ 119 | @Throws(QueryError::class) actual fun query(source: String) = Query(this, source) 120 | 121 | actual override fun equals(other: Any?) = 122 | this === other || (other is Language && self == other.self) 123 | 124 | actual override fun hashCode() = self.hashCode() 125 | 126 | override fun toString() = "Language(id=${self.rawValue}, version=$version)" 127 | } 128 | -------------------------------------------------------------------------------- /ktreesitter/src/nativeMain/kotlin/io/github/treesitter/ktreesitter/LookaheadIterator.kt: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter 2 | 3 | import io.github.treesitter.ktreesitter.internal.* 4 | import kotlin.experimental.ExperimentalNativeApi 5 | import kotlin.native.ref.createCleaner 6 | import kotlinx.cinterop.* 7 | 8 | /** 9 | * A class that is used to look up valid symbols in a specific parse state. 10 | * 11 | * Lookahead iterators can be useful to generate suggestions and improve syntax 12 | * error diagnostics. To get symbols valid in an `ERROR` node, use the lookahead 13 | * iterator on its first leaf node state. For `MISSING` nodes, a lookahead 14 | * iterator created on the previous non-extra leaf node may be appropriate. 15 | */ 16 | @OptIn(ExperimentalForeignApi::class) 17 | actual class LookaheadIterator @Throws(IllegalArgumentException::class) internal constructor( 18 | language: Language, 19 | private val state: UShort 20 | ) : AbstractIterator() { 21 | private val self = ts_lookahead_iterator_new(language.self, state) 22 | ?: throw IllegalArgumentException("State $state is not valid for $language") 23 | 24 | /** The current language of the lookahead iterator. */ 25 | actual val language: Language 26 | get() = Language(ts_lookahead_iterator_language(self)!!) 27 | 28 | @Suppress("unused") 29 | @OptIn(ExperimentalNativeApi::class) 30 | private val cleaner = createCleaner(self, ::ts_lookahead_iterator_delete) 31 | 32 | /** 33 | * The current symbol ID. 34 | * 35 | * The ID of the `ERROR` symbol is equal to `UShort.MAX_VALUE`. 36 | */ 37 | actual val currentSymbol: UShort 38 | get() = ts_lookahead_iterator_current_symbol(self) 39 | 40 | /** 41 | * The current symbol name. 42 | * 43 | * Newly created lookahead iterators will contain the `ERROR` symbol. 44 | */ 45 | actual val currentSymbolName: String 46 | get() = ts_lookahead_iterator_current_symbol_name(self)!!.toKString() 47 | 48 | /** 49 | * Reset the lookahead iterator the given [state] and, optionally, another [language]. 50 | * 51 | * @return `true` if the iterator was reset successfully or `false` if it failed. 52 | */ 53 | actual fun reset(state: UShort, language: Language?): Boolean = if (language == null) { 54 | ts_lookahead_iterator_reset_state(self, state) 55 | } else { 56 | ts_lookahead_iterator_reset(self, language.self, state) 57 | } 58 | 59 | /** Advance the lookahead iterator to the next symbol. */ 60 | actual override fun next() = super.next() 61 | 62 | /** Iterate over the symbol IDs. */ 63 | actual fun symbols(): Sequence { 64 | ts_lookahead_iterator_reset_state(self, state) 65 | return sequence { 66 | while (ts_lookahead_iterator_next(self)) { 67 | yield(ts_lookahead_iterator_current_symbol(self)) 68 | } 69 | } 70 | } 71 | 72 | /** Iterate over the symbol names. */ 73 | actual fun symbolNames(): Sequence { 74 | ts_lookahead_iterator_reset_state(self, state) 75 | return sequence { 76 | while (ts_lookahead_iterator_next(self)) { 77 | yield(ts_lookahead_iterator_current_symbol_name(self)!!.toKString()) 78 | } 79 | } 80 | } 81 | 82 | override fun computeNext() = if (ts_lookahead_iterator_next(self)) { 83 | val id = ts_lookahead_iterator_current_symbol(self) 84 | val name = ts_lookahead_iterator_current_symbol_name(self) 85 | setNext(Symbol(id, name!!.toKString())) 86 | } else { 87 | done() 88 | } 89 | 90 | operator fun iterator() = apply { reset(state) } 91 | 92 | /** A class that pairs a symbol ID with its name. */ 93 | actual data class Symbol actual constructor(actual val id: UShort, actual val name: String) 94 | } 95 | -------------------------------------------------------------------------------- /ktreesitter/src/nativeMain/kotlin/io/github/treesitter/ktreesitter/Tree.kt: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter 2 | 3 | import cnames.structs.TSTree 4 | import io.github.treesitter.ktreesitter.internal.* 5 | import kotlin.experimental.ExperimentalNativeApi 6 | import kotlin.native.ref.createCleaner 7 | import kotlinx.cinterop.* 8 | 9 | /** A class that represents a syntax tree. */ 10 | @OptIn(ExperimentalForeignApi::class) 11 | actual class Tree internal constructor( 12 | internal val self: CPointer, 13 | private var source: String?, 14 | /** The language that was used to parse the syntax tree. */ 15 | actual val language: Language 16 | ) { 17 | @Suppress("unused") 18 | @OptIn(ExperimentalNativeApi::class) 19 | private val cleaner = createCleaner(self, ::ts_tree_delete) 20 | 21 | /** The root node of the syntax tree. */ 22 | actual val rootNode = Node(ts_tree_root_node(self), this) 23 | 24 | /** The included ranges of the syntax tree. */ 25 | actual val includedRanges by lazy { 26 | memScoped { 27 | val length = alloc() 28 | val ranges = ts_tree_included_ranges(self, length.ptr) ?: return@lazy emptyList() 29 | val result = List(length.value.convert()) { ranges[it].convert() } 30 | kts_free(ranges) 31 | result 32 | } 33 | } 34 | 35 | /** 36 | * Get the root node of the syntax tree, but with 37 | * its position shifted forward by the given offset. 38 | */ 39 | actual fun rootNodeWithOffset(bytes: UInt, extent: Point): Node? { 40 | val offsetExtent = cValue { from(extent) } 41 | return ts_tree_root_node_with_offset(self, bytes, offsetExtent).convert(this) 42 | } 43 | 44 | /** 45 | * Edit the syntax tree to keep it in sync 46 | * with source code that has been modified. 47 | */ 48 | actual fun edit(edit: InputEdit) { 49 | val inputEdit = cValue { from(edit) } 50 | ts_tree_edit(self, inputEdit) 51 | source = null 52 | } 53 | 54 | /** 55 | * Create a shallow copy of the syntax tree. 56 | * 57 | * You need to copy a syntax tree in order to use it on multiple 58 | * threads or coroutines, as syntax trees are not thread safe. 59 | */ 60 | actual fun copy() = Tree(ts_tree_copy(self)!!, source, language) 61 | 62 | /** Create a new tree cursor starting from the node of the tree. */ 63 | actual fun walk() = TreeCursor(rootNode) 64 | 65 | /** Get the source code of the syntax tree, if available. */ 66 | actual fun text(): CharSequence? = source 67 | 68 | /** 69 | * Compare an old edited syntax tree to a new 70 | * syntax tree representing the same document. 71 | * 72 | * For this to work correctly, this tree must have been 73 | * edited such that its ranges match up to the new tree. 74 | * 75 | * @return A list of ranges whose syntactic structure has changed. 76 | */ 77 | actual fun changedRanges(newTree: Tree): List = memScoped { 78 | val length = alloc() 79 | val ranges = ts_tree_get_changed_ranges(self, newTree.self, length.ptr) 80 | if (length.value == 0U || ranges == null) return emptyList() 81 | val result = List(length.value.convert()) { ranges[it].convert() } 82 | kts_free(ranges) 83 | return result 84 | } 85 | 86 | override fun toString() = "Tree(language=$language, source=$source)" 87 | } 88 | -------------------------------------------------------------------------------- /ktreesitter/src/nativeMain/kotlin/io/github/treesitter/ktreesitter/TreeCursor.kt: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter 2 | 3 | import io.github.treesitter.ktreesitter.internal.* 4 | import kotlin.experimental.ExperimentalNativeApi 5 | import kotlin.native.ref.createCleaner 6 | import kotlinx.cinterop.* 7 | 8 | /** A class that can be used to efficiently walk a [syntax tree][Tree]. */ 9 | @OptIn(ExperimentalForeignApi::class) 10 | actual class TreeCursor private constructor( 11 | private val self: CPointer, 12 | internal actual val tree: Tree 13 | ) { 14 | internal constructor(node: Node) : this(ts_tree_cursor_new(node.self).ptr, node.tree) { 15 | internalNode = node 16 | } 17 | 18 | @Suppress("unused") 19 | @OptIn(ExperimentalNativeApi::class) 20 | private val cleaner = createCleaner(self) { 21 | ts_tree_cursor_delete(it) 22 | kts_free(it) 23 | } 24 | 25 | private var internalNode: Node? = null 26 | 27 | /** The current node of the cursor. */ 28 | actual val currentNode: Node 29 | get() { 30 | if (internalNode == null) 31 | internalNode = ts_tree_cursor_current_node(self).convert(tree) 32 | return internalNode!! 33 | } 34 | 35 | /** 36 | * The depth of the cursor's current node relative to the 37 | * original node that the cursor was constructed with. 38 | */ 39 | actual val currentDepth: UInt 40 | get() = ts_tree_cursor_current_depth(self) 41 | 42 | /** 43 | * The field ID of the tree cursor's current node, or `0`. 44 | * 45 | * @see [Node.childByFieldId] 46 | * @see [Language.fieldIdForName] 47 | */ 48 | actual val currentFieldId: UShort 49 | get() = ts_tree_cursor_current_field_id(self) 50 | 51 | /** 52 | * The field name of the tree cursor's current node, if available. 53 | * 54 | * @see [Node.childByFieldName] 55 | */ 56 | actual val currentFieldName: String? 57 | get() = ts_tree_cursor_current_field_name(self)?.toKString() 58 | 59 | /** 60 | * The index of the cursor's current node out of all the descendants 61 | * of the original node that the cursor was constructed with. 62 | */ 63 | actual val currentDescendantIndex: UInt 64 | get() = ts_tree_cursor_current_descendant_index(self) 65 | 66 | /** Create a shallow copy of the tree cursor. */ 67 | actual fun copy() = TreeCursor(ts_tree_cursor_copy(self).ptr, tree) 68 | 69 | /** Reset the cursor to start at a different node. */ 70 | actual fun reset(node: Node) { 71 | ts_tree_cursor_reset(self, node.self) 72 | internalNode = null 73 | } 74 | 75 | /** Reset the cursor to start at the same position as another cursor. */ 76 | actual fun reset(cursor: TreeCursor) { 77 | ts_tree_cursor_reset_to(self, cursor.self) 78 | internalNode = null 79 | } 80 | 81 | /** 82 | * Move the cursor to the first child of its current node. 83 | * 84 | * @return 85 | * `true` if the cursor successfully moved, 86 | * or `false` if there were no children. 87 | */ 88 | actual fun gotoFirstChild(): Boolean { 89 | val result = ts_tree_cursor_goto_first_child(self) 90 | if (result) internalNode = null 91 | return result 92 | } 93 | 94 | /** 95 | * Move the cursor to the last child of its current node. 96 | * 97 | * @return 98 | * `true` if the cursor successfully moved, 99 | * or `false` if there were no children. 100 | */ 101 | actual fun gotoLastChild(): Boolean { 102 | val result = ts_tree_cursor_goto_last_child(self) 103 | if (result) internalNode = null 104 | return result 105 | } 106 | 107 | /** 108 | * Move the cursor to the parent of its current node. 109 | * 110 | * @return 111 | * `true` if the cursor successfully moved, 112 | * or `false` if there was no parent node. 113 | */ 114 | actual fun gotoParent(): Boolean { 115 | val result = ts_tree_cursor_goto_parent(self) 116 | if (result) internalNode = null 117 | return result 118 | } 119 | 120 | /** 121 | * Move the cursor to the next sibling of its current node. 122 | * 123 | * @return 124 | * `true` if the cursor successfully moved, 125 | * or `false` if there was no next sibling node. 126 | */ 127 | actual fun gotoNextSibling(): Boolean { 128 | val result = ts_tree_cursor_goto_next_sibling(self) 129 | if (result) internalNode = null 130 | return result 131 | } 132 | 133 | /** 134 | * Move the cursor to the previous sibling of its current node. 135 | * 136 | * This function may be slower than [gotoNextSibling] due to how node positions 137 | * are stored. In the worst case, this will need to iterate through all the 138 | * children up to the previous sibling node to recalculate its position. 139 | * 140 | * @return 141 | * `true` if the cursor successfully moved, 142 | * or `false` if there was no previous sibling node. 143 | */ 144 | actual fun gotoPreviousSibling(): Boolean { 145 | val result = ts_tree_cursor_goto_previous_sibling(self) 146 | if (result) internalNode = null 147 | return result 148 | } 149 | 150 | /** 151 | * Move the cursor to the node that is the nth descendant of 152 | * the original node that the cursor was constructed with, 153 | * where `0` represents the original node itself. 154 | */ 155 | actual fun gotoDescendant(index: UInt) { 156 | ts_tree_cursor_goto_descendant(self, index) 157 | internalNode = null 158 | } 159 | 160 | /** 161 | * Move the cursor to the first child of its current 162 | * node that extends beyond the given byte offset. 163 | * 164 | * @return The index of the child node, or `null` if no such child was found. 165 | */ 166 | actual fun gotoFirstChildForByte(byte: UInt): UInt? { 167 | val index = ts_tree_cursor_goto_first_child_for_byte(self, byte) 168 | if (index == -1L) return null 169 | internalNode = null 170 | return index.convert() 171 | } 172 | 173 | /** 174 | * Move the cursor to the first child of its current 175 | * node that extends beyond the given point offset. 176 | * 177 | * @return The index of the child node, or `null` if no such child was found. 178 | */ 179 | actual fun gotoFirstChildForPoint(point: Point): UInt? { 180 | val pointValue = cValue { from(point) } 181 | val index = ts_tree_cursor_goto_first_child_for_point(self, pointValue) 182 | if (index == -1L) return null 183 | internalNode = null 184 | return index.convert() 185 | } 186 | 187 | override fun toString() = "TreeCursor(tree=$tree)" 188 | } 189 | -------------------------------------------------------------------------------- /ktreesitter/src/nativeMain/kotlin/io/github/treesitter/ktreesitter/Utils.kt: -------------------------------------------------------------------------------- 1 | package io.github.treesitter.ktreesitter 2 | 3 | import io.github.treesitter.ktreesitter.internal.* 4 | import kotlinx.cinterop.* 5 | 6 | @ExperimentalForeignApi 7 | internal inline fun TSInputEdit.from(edit: InputEdit) = apply { 8 | start_byte = edit.startByte 9 | old_end_byte = edit.oldEndByte 10 | new_end_byte = edit.newEndByte 11 | start_point.from(edit.startPoint) 12 | old_end_point.from(edit.oldEndPoint) 13 | new_end_point.from(edit.newEndPoint) 14 | } 15 | 16 | @ExperimentalForeignApi 17 | internal inline fun TSPoint.from(point: Point) = apply { 18 | row = point.row 19 | column = point.column 20 | } 21 | 22 | @ExperimentalForeignApi 23 | internal inline fun TSRange.from(range: Range) = apply { 24 | start_point.from(range.startPoint) 25 | end_point.from(range.endPoint) 26 | start_byte = range.startByte 27 | end_byte = range.endByte 28 | } 29 | 30 | @ExperimentalForeignApi 31 | internal inline fun TSPoint.convert() = Point(row, column) 32 | 33 | @ExperimentalForeignApi 34 | internal inline fun TSRange.convert() = 35 | Range(start_point.convert(), end_point.convert(), start_byte, end_byte) 36 | 37 | @ExperimentalForeignApi 38 | internal inline fun CValue.convert(tree: Tree) = 39 | if (ts_node_is_null(this)) null else Node(this, tree) 40 | 41 | @ExperimentalForeignApi 42 | internal inline val CValue.ptr: CPointer 43 | get() = place(kts_malloc(sizeOf().convert())!!.reinterpret()) 44 | -------------------------------------------------------------------------------- /languages/java/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import java.io.OutputStream.nullOutputStream 2 | import org.gradle.internal.os.OperatingSystem 3 | import org.gradle.kotlin.dsl.support.useToRun 4 | import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi 5 | import org.jetbrains.kotlin.gradle.tasks.CInteropProcess 6 | import org.jetbrains.kotlin.konan.target.PlatformManager 7 | 8 | inline val File.unixPath: String 9 | get() = if (!os.isWindows) path else path.replace("\\", "/") 10 | 11 | val os: OperatingSystem = OperatingSystem.current() 12 | val libsDir = layout.buildDirectory.get().dir("libs") 13 | val grammarDir = projectDir.resolve("tree-sitter-java") 14 | 15 | version = grammarDir.resolve("Makefile").readLines() 16 | .first { it.startsWith("VERSION := ") }.removePrefix("VERSION := ") 17 | 18 | plugins { 19 | `maven-publish` 20 | signing 21 | alias(libs.plugins.kotlin.mpp) 22 | alias(libs.plugins.android.library) 23 | id("io.github.tree-sitter.ktreesitter-plugin") 24 | } 25 | 26 | grammar { 27 | baseDir = grammarDir 28 | grammarName = project.name 29 | className = "TreeSitterJava" 30 | packageName = "io.github.treesitter.ktreesitter.java" 31 | files = arrayOf( 32 | // grammarDir.resolve("src/scanner.c"), 33 | grammarDir.resolve("src/parser.c") 34 | ) 35 | } 36 | 37 | val generateTask = tasks.generateGrammarFiles.get() 38 | 39 | kotlin { 40 | jvm {} 41 | 42 | androidTarget { 43 | withSourcesJar(true) 44 | publishLibraryVariants("release") 45 | } 46 | 47 | when { 48 | os.isLinux -> listOf(linuxX64(), linuxArm64()) 49 | os.isWindows -> listOf(mingwX64()) 50 | os.isMacOsX -> listOf( 51 | macosArm64(), 52 | macosX64(), 53 | iosArm64(), 54 | iosSimulatorArm64() 55 | ) 56 | else -> { 57 | val arch = System.getProperty("os.arch") 58 | throw GradleException("Unsupported platform: $os ($arch)") 59 | } 60 | }.forEach { target -> 61 | target.compilations.configureEach { 62 | cinterops.create(grammar.interopName.get()) { 63 | defFileProperty.set(generateTask.interopFile.asFile) 64 | includeDirs.allHeaders(grammarDir.resolve("bindings/c")) 65 | extraOpts("-libraryPath", libsDir.dir(konanTarget.name)) 66 | tasks.getByName(interopProcessingTaskName).mustRunAfter(generateTask) 67 | } 68 | } 69 | } 70 | 71 | jvmToolchain(17) 72 | 73 | sourceSets { 74 | val generatedSrc = generateTask.generatedSrc.get() 75 | configureEach { 76 | kotlin.srcDir(generatedSrc.dir(name).dir("kotlin")) 77 | } 78 | 79 | commonMain { 80 | @OptIn(ExperimentalKotlinGradlePluginApi::class) 81 | languageSettings { 82 | compilerOptions { 83 | freeCompilerArgs.add("-Xexpect-actual-classes") 84 | } 85 | } 86 | 87 | dependencies { 88 | implementation(libs.kotlin.stdlib) 89 | } 90 | } 91 | 92 | jvmMain { 93 | resources.srcDir(generatedSrc.dir(name).dir("resources")) 94 | } 95 | } 96 | } 97 | 98 | android { 99 | namespace = "io.github.treesitter.ktreesitter.${grammar.grammarName.get()}" 100 | compileSdk = (property("sdk.version.compile") as String).toInt() 101 | ndkVersion = property("ndk.version") as String 102 | defaultConfig { 103 | minSdk = (property("sdk.version.min") as String).toInt() 104 | ndk { 105 | moduleName = grammar.libraryName.get() 106 | //noinspection ChromeOsAbiSupport 107 | abiFilters += setOf("x86_64", "arm64-v8a", "armeabi-v7a") 108 | } 109 | resValue("string", "version", version as String) 110 | } 111 | externalNativeBuild { 112 | cmake { 113 | path = generateTask.cmakeListsFile.get().asFile 114 | buildStagingDirectory = file(".cmake") 115 | version = property("cmake.version") as String 116 | } 117 | } 118 | compileOptions { 119 | sourceCompatibility = JavaVersion.VERSION_17 120 | targetCompatibility = JavaVersion.VERSION_17 121 | } 122 | } 123 | 124 | tasks.withType().configureEach { 125 | if (name.startsWith("cinteropTest")) return@configureEach 126 | 127 | val grammarFiles = grammar.files.get() 128 | val grammarName = grammar.grammarName.get() 129 | val runKonan = File(konanHome.get()).resolve("bin") 130 | .resolve(if (os.isWindows) "run_konan.bat" else "run_konan").path 131 | val libFile = libsDir.dir(konanTarget.name).file("libtree-sitter-$grammarName.a").asFile 132 | val objectFiles = grammarFiles.map { 133 | grammarDir.resolve(it.nameWithoutExtension + ".o").path 134 | }.toTypedArray() 135 | val loader = PlatformManager(konanHome.get(), false, konanDataDir.orNull).loader(konanTarget) 136 | 137 | doFirst { 138 | if (!File(loader.absoluteTargetToolchain).isDirectory) loader.downloadDependencies() 139 | 140 | val argsFile = File.createTempFile("args", null) 141 | argsFile.deleteOnExit() 142 | argsFile.writer().useToRun { 143 | write("-I" + grammarDir.resolve("src").unixPath + "\n") 144 | write("-DTREE_SITTER_HIDE_SYMBOLS\n") 145 | write("-fvisibility=hidden\n") 146 | write("-std=c11\n") 147 | write("-O2\n") 148 | write("-g\n") 149 | write("-c\n") 150 | grammarFiles.forEach { write(it.unixPath + "\n") } 151 | } 152 | 153 | exec { 154 | executable = runKonan 155 | workingDir = grammarDir 156 | standardOutput = nullOutputStream() 157 | args("clang", "clang", konanTarget.name, "@" + argsFile.path) 158 | } 159 | 160 | exec { 161 | executable = runKonan 162 | workingDir = grammarDir 163 | standardOutput = nullOutputStream() 164 | args("llvm", "llvm-ar", "rcs", libFile.path, *objectFiles) 165 | } 166 | } 167 | 168 | inputs.files(*grammarFiles) 169 | outputs.file(libFile) 170 | } 171 | 172 | tasks.create("javadocJar") { 173 | group = "documentation" 174 | archiveClassifier.set("javadoc") 175 | } 176 | 177 | publishing { 178 | publications.withType(MavenPublication::class) { 179 | val grammarName = grammar.grammarName.get() 180 | artifactId = grammar.libraryName.get() 181 | artifact(tasks["javadocJar"]) 182 | pom { 183 | name.set("KTreeSitter $grammarName") 184 | description.set("$grammarName grammar for KTreeSitter") 185 | licenses { 186 | license { 187 | name.set("MIT License") 188 | url.set("https://spdx.org/licenses/MIT.html") 189 | } 190 | } 191 | developers { 192 | developer { 193 | id.set("ObserverOfTime") 194 | email.set("chronobserver@disroot.org") 195 | url.set("https://github.com/ObserverOfTime") 196 | } 197 | } 198 | scm { 199 | url.set("https://github.com/tree-sitter/kotlin-tree-sitter") 200 | connection.set("scm:git:git://github.com/tree-sitter/kotlin-tree-sitter.git") 201 | developerConnection.set( 202 | "scm:git:ssh://github.com/tree-sitter/kotlin-tree-sitter.git" 203 | ) 204 | } 205 | } 206 | } 207 | 208 | repositories { 209 | maven { 210 | name = "local" 211 | url = uri(layout.buildDirectory.dir("repo")) 212 | } 213 | } 214 | } 215 | 216 | signing { 217 | isRequired = System.getenv("CI") != null 218 | if (isRequired) { 219 | val key = System.getenv("SIGNING_KEY") 220 | val password = System.getenv("SIGNING_PASSWORD") 221 | useInMemoryPgpKeys(key, password) 222 | } 223 | sign(publishing.publications) 224 | } 225 | 226 | tasks.withType().configureEach { 227 | mustRunAfter(tasks.withType()) 228 | } 229 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "ktreesitter" 2 | 3 | pluginManagement { 4 | includeBuild("ktreesitter-plugin") 5 | repositories { 6 | google() 7 | mavenCentral() 8 | gradlePluginPortal() 9 | } 10 | } 11 | 12 | include(":ktreesitter") 13 | 14 | file("languages").listFiles { file -> file.isDirectory }?.forEach { 15 | include(":languages:${it.name}") 16 | } 17 | --------------------------------------------------------------------------------