├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── actions │ └── setup │ │ └── action.yml ├── dependabot.yml └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .rusty-hook.toml ├── .vscode ├── launch.json └── tasks.json ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── DOCS.md ├── LICENSE ├── README.md ├── TEST_CASES.md ├── Taskfile.yml ├── demo.gif ├── language-server ├── Cargo.toml └── src │ ├── analysis.rs │ ├── builtins.rs │ ├── capabilities.rs │ ├── config.rs │ ├── helpers.rs │ ├── lsp.rs │ ├── main.rs │ └── update_checker.rs ├── package-lock.json ├── pest.tmLanguage ├── sublime-text └── pest.tmLanguage └── vscode ├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── client ├── src │ ├── index.ts │ └── server.ts └── tsconfig.json ├── icon.png ├── language-configuration.json ├── package-lock.json ├── package.json ├── syntaxes └── pest.tmLanguage.json ├── tests ├── .vscode │ └── settings.json ├── cjk.pest ├── json.pest ├── misc.pest └── no_consume.pest └── tsconfig.json /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @pest-parser/pest-vscode -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## **Describe the bug** 11 | 12 | A clear and concise description of what the bug is. 13 | 14 | ## **To Reproduce** 15 | 16 | Steps to reproduce the behavior: 17 | 18 | 1. Type '...' 19 | 2. Run '....' 20 | 3. See error 21 | 22 | ### **Reproduction File** 23 | 24 | If relevant, please include the reproduction file inside a code block below. 25 | 26 | ### **Log** 27 | 28 | If you think it will help, please include the server log in a drop-down below. It can be found, for example in vscode, in the `Output` tab. 29 | 30 | ## **Expected behavior** 31 | 32 | A clear and concise description of what you expected to happen. 33 | 34 | ## **Screenshots** 35 | 36 | If applicable, add screenshots or GIFs to help explain your problem. 37 | 38 | ## **Environment (please complete the following information):** 39 | 40 | - OS: [e.g. Windows] 41 | - Editor [e.g. vscode, vim] 42 | - Server Version [e.g. 0.2.0] 43 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## **Is your feature request related to a problem? Please describe.** 11 | 12 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 13 | 14 | ## **Describe the solution you'd like** 15 | 16 | A clear and concise description of what you want to happen. 17 | -------------------------------------------------------------------------------- /.github/actions/setup/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup 2 | description: Initial setup for workflows 3 | 4 | inputs: 5 | kind: 6 | description: Job kind (for cache key) 7 | required: false 8 | secret: 9 | description: GitHub Token 10 | required: true 11 | rust-target: 12 | description: Target to install using rustup, or nothing for a default target 13 | required: false 14 | outputs: 15 | cache-key: 16 | description: Cache key 17 | value: ${{ inputs.kind }}-${{ runner.os }}-${{ steps.toolchain.outputs.cachekey }} 18 | 19 | runs: 20 | using: composite 21 | steps: 22 | - name: Install Wasmpack 23 | shell: bash 24 | run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh 25 | 26 | - name: Setup Node 27 | uses: actions/setup-node@v4 28 | with: 29 | node-version: 18 30 | cache: npm 31 | cache-dependency-path: vscode/package-lock.json 32 | 33 | - name: NPM Install 34 | shell: bash 35 | working-directory: vscode 36 | run: npm install 37 | 38 | - name: Setup Task 39 | uses: arduino/setup-task@v2 40 | with: 41 | repo-token: ${{ inputs.secret }} 42 | 43 | - name: Install Rust Target 44 | shell: bash 45 | if: ${{ inputs.rust-target }} 46 | run: rustup target add ${{ inputs.rust-target }} 47 | 48 | - name: Set up cache 49 | uses: Swatinem/rust-cache@v2 50 | with: 51 | shared-key: ${{ inputs.kind }}-${{ runner.os }}-${{ steps.toolchain.outputs.cachekey }} 52 | workspaces: language-server 53 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | assignees: 13 | - "Jamalam360" 14 | groups: 15 | rust-dependencies: 16 | applies-to: version-updates 17 | patterns: 18 | - "*" 19 | 20 | - package-ecosystem: "npm" # See documentation for possible values 21 | directory: "/vscode" # Location of package manifests 22 | schedule: 23 | interval: "weekly" 24 | assignees: 25 | - "Jamalam360" 26 | groups: 27 | dev-dependencies: 28 | applies-to: version-updates 29 | patterns: 30 | - "@trivago/prettier-plugin-sort-imports" 31 | - "@typescript-eslint/*" 32 | - "esbuild" 33 | - "eslint-*" 34 | - "gts" 35 | - "ovsx" 36 | - "@vscode/vsce" 37 | ignore: 38 | # we only want to update these when they have a new feature we want 39 | - dependency-name: "vscode-languageclient" 40 | - dependency-name: "@types/node" 41 | - dependency-name: "@types/vscode" 42 | 43 | - package-ecosystem: "github-actions" 44 | directory: "/" 45 | schedule: 46 | interval: "weekly" 47 | assignees: 48 | - "Jamalam360" 49 | groups: 50 | gha-dependencies: 51 | applies-to: version-updates 52 | patterns: 53 | - "*" 54 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | pull_request: 8 | 9 | jobs: 10 | check: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | os: [ubuntu-latest, windows-latest, macos-latest] 16 | steps: 17 | - name: Checkout Repository 18 | uses: actions/checkout@v4 19 | 20 | - name: Setup 21 | uses: ./.github/actions/setup 22 | with: 23 | kind: check 24 | secret: ${{ secrets.GITHUB_TOKEN }} 25 | 26 | - name: Format and Lint 27 | if: contains(matrix.os, 'ubuntu') 28 | run: task fmt-and-lint 29 | 30 | - name: Cargo Check 31 | run: cargo check 32 | 33 | package-vscode: 34 | runs-on: ubuntu-latest 35 | steps: 36 | - name: Checkout Repository 37 | uses: actions/checkout@v4 38 | 39 | - name: Setup 40 | uses: ./.github/actions/setup 41 | with: 42 | kind: package-vscode 43 | secret: ${{ secrets.GITHUB_TOKEN }} 44 | 45 | - name: Package Extension 46 | working-directory: vscode 47 | run: npm run package 48 | 49 | - name: Upload Extension 50 | uses: actions/upload-artifact@v4 51 | with: 52 | name: pest-vscode 53 | path: vscode/pest.vsix 54 | 55 | package-sublime-text: 56 | runs-on: ubuntu-latest 57 | steps: 58 | - name: Checkout Repository 59 | uses: actions/checkout@v4 60 | 61 | - name: Package Sublime Text Package 62 | working-directory: sublime-text 63 | run: zip pest.sublime-package * 64 | 65 | - name: Upload Extension 66 | uses: actions/upload-artifact@v4 67 | with: 68 | name: pest-sublime-text 69 | path: sublime-text/pest.sublime-package 70 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release Extensions 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | # Gives access to the VSCE_PAT, OVSX_TOKEN secret 12 | environment: vsce 13 | permissions: 14 | # Allows creation of releases 15 | contents: write 16 | outputs: 17 | release_id: ${{ steps.create_release.outputs.id }} 18 | steps: 19 | - name: Checkout Repository 20 | uses: actions/checkout@v4 21 | 22 | - name: Setup 23 | uses: ./.github/actions/setup 24 | with: 25 | kind: release 26 | secret: ${{ secrets.GITHUB_TOKEN }} 27 | 28 | - name: Install VSCode NPM Packages 29 | working-directory: vscode 30 | run: npm install 31 | 32 | - name: Package VSCode Extension 33 | working-directory: vscode 34 | run: npm run package 35 | 36 | - name: Publish to VSCode Marketplace 37 | working-directory: vscode 38 | run: npm run publish:vsce 39 | env: 40 | VSCE_PAT: ${{ secrets.VSCE_PAT }} 41 | 42 | - name: Publish to OpenVSX 43 | working-directory: vscode 44 | run: npm run publish:ovsx 45 | env: 46 | OPENVSX_PAT: ${{ secrets.OVSX_TOKEN }} 47 | 48 | - name: Package Sublime Text Package 49 | working-directory: sublime-text 50 | run: zip pest.sublime-package * 51 | 52 | - name: Publish to crates.io 53 | working-directory: language-server 54 | run: cargo publish --token ${{ secrets.CRATES_TOKEN }} 55 | 56 | - name: Get Changelog 57 | id: get_changelog 58 | run: | 59 | EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64) 60 | echo "COMMITS<<$EOF" >> $GITHUB_OUTPUT 61 | echo "COMMITS=\"$(awk -v latest="$(grep -Eo '^## v[0-9]+\.[0-9]+\.[0-9]+$' CHANGELOG.md | head -n1)" '/^## v/ {if (header) exit; header=1} /^## v'${latest}'/{print; next} header && !/^## v/{print}' CHANGELOG.md)\"" >> $GITHUB_OUTPUT 62 | echo "$EOF" >> $GITHUB_OUTPUT 63 | 64 | - name: Create Release 65 | id: create_release 66 | uses: softprops/action-gh-release@v2 67 | env: 68 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 69 | with: 70 | tag_name: ${{ github.ref }} 71 | name: ${{ github.ref_name }} 72 | body: | 73 | # Checklist Before Publishing 74 | 75 | - [ ] Check [VSCode extension](https://marketplace.visualstudio.com/items?itemName=pest.pest-ide-tools) was published correctly. 76 | - [ ] Check [OpenVSX extension](https://open-vsx.org/extension/pest/pest-ide-tools) was published correctly. 77 | - [ ] Check [crates.io release](https://crates.io/crates/pest-language-server/versions) was published correctly. 78 | - [ ] Check artifacts were uploaded to this release. 79 | - [ ] Update release body. 80 | 81 | ${{ steps.get_changelog.outputs.COMMITS }} 82 | draft: true 83 | prerelease: false 84 | files: | 85 | vscode/pest.vsix 86 | sublime-text/pest.sublime-package 87 | 88 | build-binaries: 89 | needs: release 90 | runs-on: ${{ matrix.target.runner }} 91 | 92 | permissions: 93 | # So we can upload to the release 94 | contents: write 95 | 96 | strategy: 97 | matrix: 98 | target: 99 | [ 100 | { runner: "macos-14", target: "aarch64-apple-darwin", os: darwin, arch: aarch64 }, # Apple silicon 101 | { runner: "ubuntu-latest", target: "aarch64-unknown-linux-gnu", os: linux, arch: aarch64 }, 102 | { runner: "macos-12", target: "x86_64-apple-darwin", os: darwin, arch: x86_64 }, # Intel Mac 103 | { runner: "ubuntu-latest", target: "x86_64-pc-windows-gnu", os: windows, arch: x86_64 }, # It's trivial to cross-compile to Windows (less so for Mac) 104 | { runner: "ubuntu-latest", target: "x86_64-unknown-linux-gnu", os: linux, arch: x86_64 }, 105 | ] 106 | 107 | steps: 108 | - name: Checkout code 109 | uses: actions/checkout@v4 110 | 111 | - name: Setup 112 | uses: ./.github/actions/setup 113 | with: 114 | kind: release-compile-binaries 115 | secret: ${{ secrets.GITHUB_TOKEN }} 116 | rust-target: ${{ matrix.target.target }} 117 | 118 | - name: Set up Windows 119 | if: matrix.target.os == 'windows' 120 | run: sudo apt-get install -y --no-install-recommends mingw-w64 musl-tools gcc-mingw-w64-x86-64-win32 121 | 122 | - name: Set up aarch64 Linux 123 | if: matrix.target.target == 'aarch64-unknown-linux-gnu' 124 | run: | 125 | sudo apt-get install gcc-aarch64-linux-gnu 126 | echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV 127 | 128 | - name: Build binary 129 | run: cargo build --release --target=${{ matrix.target.target }} 130 | 131 | - name: Package binary (Linux and Mac) 132 | if: matrix.target.os != 'windows' 133 | run: tar -zcvf pest-language-server-${{ matrix.target.os }}-${{ matrix.target.arch }}.tar.gz -C target/${{ matrix.target.target }}/release pest-language-server 134 | 135 | - name: Package binary (Windows) 136 | if: matrix.target.os == 'windows' 137 | run: tar -zcvf pest-language-server-${{ matrix.target.os }}-${{ matrix.target.arch }}.tar.gz -C target/${{ matrix.target.target }}/release pest-language-server.exe 138 | 139 | - uses: xresloader/upload-to-github-release@v1 140 | env: 141 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 142 | with: 143 | file: "./pest-language-server-${{ matrix.target.os }}-${{ matrix.target.arch }}.tar.gz" 144 | release_id: ${{ needs.release.outputs.release_id }} 145 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .task/ 2 | node_modules 3 | target 4 | *.sublime-package 5 | -------------------------------------------------------------------------------- /.rusty-hook.toml: -------------------------------------------------------------------------------- 1 | [hooks] 2 | pre-commit = "task fmt-and-lint" 3 | 4 | [logging] 5 | verbose = true 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "debugWebWorkerHost": true, 13 | "args": [ 14 | "${workspaceFolder}/vscode/tests", 15 | "--disable-extensions", 16 | "--extensionDevelopmentPath=${workspaceFolder}/vscode" 17 | ], 18 | "outFiles": [ 19 | "${workspaceFolder}/vscode/build/**/*" 20 | ], 21 | "preLaunchTask": "npm: esbuild-with-rust - vscode" 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "esbuild-with-rust", 7 | "path": "vscode", 8 | "group": "build", 9 | "problemMatcher": [], 10 | "label": "npm: esbuild-with-rust - vscode", 11 | "detail": "task rust-build -d .. && npm run esbuild-client" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes will be documented in this file. 4 | 5 | 6 | 7 | ## v0.3.11 8 | 9 | - fix(ci): update Node version for OpenVSX publishing. 10 | 11 | ## v0.3.10 12 | 13 | - [[sbillig](https://github.com/sbillig)] feat(analysis): don't emit unused rule warning if the rule name begins with an underscore (#88) 14 | 15 | ## v0.3.9 16 | 17 | - fix(lsp): use rustls rather than native-tls for reqwest 18 | 19 | ## v0.3.8 20 | 21 | - feat(ci): publish binaries to releases, fixes #51 22 | - chore(lsp): update dependencies 23 | 24 | ## v0.3.7 25 | 26 | - feat(deps): update all deps, fixes #50 27 | 28 | ## v0.3.6 29 | 30 | - [[Fluffyalien1422](https://github.com/Fluffyalien1422)] fix(vscode): add quotes around path when invoking the LS (#41) 31 | 32 | ## v0.3.5 33 | 34 | - fix(ci): fix the publish workflow 35 | 36 | ## v0.3.4 37 | 38 | - [[notpeter](https://github.com/notpeter)] fix(server): off-by-one error (#37) 39 | - [[notpeter](https://github.com/notpeter)] fix(grammar): issues with highlighting rules that started with a built-in's name (#38) 40 | 41 | ## v0.3.3 42 | 43 | - fix(server): hotfix for #28, ranges (along with other tokens like insensitive strings) no longer crash the server 44 | 45 | A proper fix for the code causing the crash in #28 will be released, but I do not have the time at the moment. 46 | 47 | ## v0.3.2 48 | 49 | - fix(vscode): update checker is now enabled by default, and some of its logic 50 | has been modified and fixed. It also supports cancellation of the install task 51 | - fix(vscode): give defaults to all config options 52 | - fix(server): fix crash with code actions 53 | 54 | ## v0.3.1 55 | 56 | - revert(server): revert performance marks temporarily, while they are 57 | refactored into a more generic crate 58 | 59 | ## v0.3.0 60 | 61 | - feat(server): add performance marks for debugging 62 | - feat(server): simple rule extraction support 63 | - fix(server): validate AST to catch errors like non-progressing expressions 64 | 65 | ## v0.2.2 66 | 67 | - feat(vscode): allow relative paths in `pestIdeTools.serverPath` 68 | - fix(vscode): allow `pestIdeTools.serverPath` to be `null` in the schema 69 | - fix(server): CJK/non-ascii characters no longer crash the server 70 | - fix(server): add a CJK test case to the manual testing recommendations 71 | 72 | ## v0.2.1 73 | 74 | - fix(vscode): scan both stdout and stderr of Cargo commands, fixes some issues 75 | with installation flow 76 | - feat(*): documentation, issue templates 77 | - feat(sublime): begin publishing a sublime text package 78 | - fix(server, vscode): server now hot-reloads config updates more reliably 79 | - fix(server, vscode): bump problematic dependencies (love the JS ecosystem...a 80 | CVE a day keeps the doctor away) 81 | - feat(server): add rule inlining code action 82 | - feat(server): ignore unused rule name analysis if there is only one unused 83 | rule (hack fix) 84 | 85 | ## v0.2.0 86 | 87 | - feat(*): port to tower lsp 88 | - This will allow the usage of this LS by other IDEs. 89 | - The vscode extension will prompt you to download the server. 90 | - Other IDEs will have to have the LS installed via `cargo install`. 91 | - feat(*): add configuration options 92 | - feat(server, #6): diagnostic for unused rules 93 | - feat(server, #7): show rule docs (`///`) on hover 94 | - fix(server, #8): solve issue relating to 0 vs 1 indexing causing diagnostics 95 | to occur at the wrong locations 96 | - feat(server): add a version checker 97 | - feat(readme, #2): update readme and add demo gif 98 | - feat(ci, #4): automatically populate changelog 99 | - fix(ci): lint all rust code 100 | 101 | ## v0.1.2 102 | 103 | - feat: upgrade pest v2.5.6, pest-fmt v0.2.3. See 104 | [Release Notes](https://github.com/pest-parser/pest/releases/tag/v2.5.6). 105 | - fix(server): solve issue relating to 0 vs 1 indexing. 106 | - feat(server): suggest user-defined rule names in intellisense. 107 | 108 | ## v0.1.1 109 | 110 | - feat(server): add hover information for `SOI` and `EOI` 111 | - fix(ci): allow the release workflow to create releases. 112 | - fix(vscode): add a readme for the vscode extension. 113 | - fix(vscode): add a changelog. 114 | 115 | ## v0.1.0 116 | 117 | - Initial release 118 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | resolver = "2" 3 | members = ["language-server"] 4 | -------------------------------------------------------------------------------- /DOCS.md: -------------------------------------------------------------------------------- 1 | # Pest IDE Tools Documentation 2 | 3 | This document contains instructions for setting up Pest support for all of the supported editors. 4 | 5 | ## Contents 6 | 7 | - [Server Configuration](#config) 8 | - [VSCode](#vscode) 9 | - [Sublime Text](#sublime-text) 10 | 11 | ## Config 12 | 13 | The method of updating your config is editor specific. 14 | 15 | The available options for all editors are: 16 | 17 | ```jsonc 18 | { 19 | // Check for updates to the Pest LS binary via crates.io 20 | "pestIdeTools.checkForUpdates": true, 21 | // Ignore specific rule names for the unused rules diagnostics (useful for specifying root rules) 22 | "pestIdeTools.alwaysUsedRuleNames": [ 23 | "rule_one", 24 | "rule_two" 25 | ] 26 | } 27 | ``` 28 | 29 | ## VSCode 30 | 31 | 1. Download [the extension](https://marketplace.visualstudio.com/items?itemName=pest.pest-ide-tools). 32 | 2. Await the prompt which will ask you if you want to install a suitable binary, and accept. 33 | 3. Wait for it to install the server. 34 | - If the server fails to install, you can install it manually using `cargo install pest-language-server`, then use the configuration `pestIdeTools.serverPath` to point the extension to the installed binary. 35 | 4. (_Optional_) You may need to execute the command `Pest: Restart server` or reload your window for the server to activate. 36 | 37 | ### VSCode Specific Configs 38 | 39 | These config options are specific to VSCode. 40 | 41 | ```jsonc 42 | { 43 | // Set a custom path to a Pest LS binary 44 | "pestIdeTools.serverPath": "/path/to/binary", 45 | // Custom arguments to pass to the Pest LS binary 46 | "pestIdeTools.customArgs": [] 47 | } 48 | ``` 49 | 50 | ## Sublime Text 51 | 52 | 1. Download the `pest.sublime-package` file from the [latest release](https://github.com/pest-parser/pest-ide-tools/releases/latest)'s assets page. 53 | - This gives you syntax highlighting for Pest flies. 54 | 2. Place the downloaded `pest.sublime-package` file in the `path/to/sublime-text/Installed Packages` directory. 55 | 3. Install the server using `cargo install pest-language-server`. 56 | 3. Execute the `Preferences: LSP Settings` command and add a new key to the `clients` object. 57 | ```json 58 | // LSP.sublime-settings 59 | "clients": { 60 | "pest": { 61 | "enabled": true, 62 | // This is usually something like /home/username/.cargo/bin/pest-language-server 63 | "command": ["/path/to/language/server/binary"], 64 | "selector": "source.pest", 65 | }, 66 | // ...other LSPs 67 | } 68 | ``` 69 | 4. You may have to restart your Sublime Text to get the LSP to start. 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pest IDE Tools 2 | 3 | _IDE support for [Pest](https://pest.rs), via the LSP._ 4 | 5 | This repository contains an implementation of the _Language Server Protocol_ in 6 | Rust, for the Pest parser generator. 7 | 8 |

9 | A demo of the Pest VSCode extension. 10 |

11 | 12 | ## Features 13 | 14 | - Error reporting. 15 | - Warnings for unused rules. 16 | - Syntax highlighting definitions available. 17 | - Rename rules. 18 | - Go to rule declaration, definition, or references. 19 | - Hover information for built-in rules and documented rules. 20 | - Autocompletion of rule names. 21 | - Inline and extract rules. 22 | - Full-unicode support. 23 | - Formatting. 24 | - Update checking. 25 | 26 | Please see the 27 | [issues page](https://github.com/pest-parser/pest-ide-tools/issues) to suggest 28 | features or view previous suggestions. 29 | 30 | ## Usage 31 | 32 | You can find documentation on how to set up the server for in the `DOCS.md` 33 | file. 34 | 35 | ### Supported IDEs 36 | 37 | - [Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=pest.pest-ide-tools) 38 | - VSCode has a pre-built extension that can compile, update, and start up the 39 | language server. It also includes syntax highlighting definitions. 40 | - The extension is also available on [OpenVSX](https://open-vsx.org/extension/pest/pest-ide-tools) 41 | - Sublime Text 42 | - Sublime Text packages can be obtained from [the latest release](https://github.com/pest-parser/pest-ide-tools/releases/latest) 43 | - Vim 44 | - Vim support is provided via the [pest.vim](https://github.com/pest-parser/pest.vim) package. 45 | - Zed 46 | - Zed support is provided via the [Zed Pest extension](https://github.com/pest-parser/zed-pest). 47 | 48 | Due to the usage of the LSP by this project, adding support for new IDEs should 49 | be far more achievable than a custom implementation for each editor. Please see 50 | the [tracking issue](https://github.com/pest-parser/pest-ide-tools/issues/10) to 51 | request support for another IDE or view the current status of IDE support. 52 | 53 | ## Development 54 | 55 | This repository uses a [Taskfile](https://taskfile.dev); install the `task` 56 | command for a better experience developing in this repository. 57 | 58 | The task `fmt-and-lint` can be used to check the formatting and lint your code 59 | to ensure it fits with the rest of the repository. 60 | 61 | In VSCode, press `F5` to build and debug the VSCode extension. This is the only 62 | method of debugging that we have pre set-up. 63 | 64 | ### Architecture 65 | 66 | The server itself is implemented in Rust using `tower-lsp`. It communicates with 67 | editors via JSON-RPC through standard input/output, according to the language 68 | server protocol. 69 | 70 | ### Contributing 71 | 72 | We appreciate contributions! I recommend reaching out on Discord (the invite to 73 | which can be found at [pest.rs](https://pest.rs)) before contributing, to check 74 | with us. 75 | 76 | ## Credits 77 | 78 | - [OsoHQ](https://github.com/osohq), for their 79 | [blog post](https://www.osohq.com/post/building-vs-code-extension-with-rust-wasm-typescript), 80 | and open source code which was used as inspiration. 81 | - [Stef Gijsberts](https://github.com/Stef-Gijsberts) for their 82 | [Pest syntax highlighting TextMate bundle](https://github.com/Stef-Gijsberts/pest-Syntax-Highlighting-for-vscode) 83 | which is used in this extension under the MIT license. 84 | -------------------------------------------------------------------------------- /TEST_CASES.md: -------------------------------------------------------------------------------- 1 | # Test Cases 2 | 3 | These are manual tests that should be performed to ensure the server is working correctly. 4 | 5 | - Check error reporting works as expected. 6 | - Check autocompletion works as expected. 7 | - Check formatting works as expected. 8 | - Check that rules with documentation show that documentation on hover. 9 | - Check that builtins show documentation on hover. 10 | - Check that the unused rule diagnostic works, with and without the `pestIdeTools.alwaysUsedRuleNames` configuration. 11 | - Check go to definition and find references works correctly. 12 | - Check that renaming rules works as expected. 13 | - Check that inlining and extracting rules works. 14 | - Check the CJK characters example works. Attempt to hover over a CJK rule to see if the server crashes. 15 | 16 | -------------------------------------------------------------------------------- /Taskfile.yml: -------------------------------------------------------------------------------- 1 | version: 3 2 | 3 | tasks: 4 | rust-fmt-and-lint: 5 | desc: Format and lint the Rust code 6 | internal: true 7 | 8 | method: checksum 9 | sources: 10 | - language-server/src/**/*.rs 11 | 12 | cmds: 13 | - cargo fmt --check 14 | - cargo clippy 15 | 16 | vscode-fmt-and-lint: 17 | desc: Format and lint the VSCode package 18 | dir: vscode 19 | internal: true 20 | 21 | method: checksum 22 | sources: 23 | - client/**/* 24 | - server/**/* 25 | - syntaxes/* 26 | - package.json 27 | 28 | cmds: 29 | - npm run fmt-check 30 | - npm run lint 31 | 32 | fmt-and-lint: 33 | desc: Format and lint all code. 34 | 35 | deps: 36 | - rust-fmt-and-lint 37 | - vscode-fmt-and-lint 38 | 39 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pest-parser/pest-ide-tools/6344e2a0fdb4af42dfdc106980fe730b818204c5/demo.gif -------------------------------------------------------------------------------- /language-server/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pest-language-server" 3 | version = "0.3.11" 4 | authors = ["Jamalam "] 5 | description = "A language server for Pest." 6 | edition = "2021" 7 | homepage = "https://pest.rs" 8 | license = "Apache-2.0" 9 | readme = "../README.md" 10 | repository = "https://github.com/pest-parser/pest-ide-tools" 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [dependencies] 15 | pest = "2.7.9" 16 | pest_fmt = "0.2.5" 17 | pest_meta = "2.7.8" 18 | reqwest = { version = "0.12.4", features = [ 19 | "json", 20 | "rustls-tls", 21 | ], default-features = false } 22 | serde = { version = "1.0.197", features = ["derive"] } 23 | serde_json = "1.0.116" 24 | tokio = { version = "1.37.0", features = ["full"] } 25 | tower-lsp = "0.20.0" 26 | unicode-segmentation = "1.11.0" 27 | 28 | [dev-dependencies] 29 | rusty-hook = "0.11" 30 | -------------------------------------------------------------------------------- /language-server/src/analysis.rs: -------------------------------------------------------------------------------- 1 | use pest::{iterators::Pairs, Span}; 2 | use pest_meta::parser::Rule; 3 | use reqwest::Url; 4 | use std::collections::HashMap; 5 | use tower_lsp::lsp_types::Location; 6 | 7 | use crate::{ 8 | builtins::BUILTINS, 9 | helpers::{FindOccurrences, IntoLocation}, 10 | }; 11 | 12 | #[derive(Debug, Clone)] 13 | /// Stores analysis information for a rule. 14 | pub struct RuleAnalysis { 15 | /// The location of the entire definition of the rule (i.e. `rule = { "hello" }`). 16 | pub definition_location: Location, 17 | /// The location of the name definition of the rule. 18 | pub identifier_location: Location, 19 | /// The tokens that make up the rule. 20 | pub tokens: Vec<(String, Location)>, 21 | /// The rules expression, in [String] form. 22 | pub expression: String, 23 | /// The occurrences of the rule, including its definition. 24 | pub occurrences: Vec, 25 | /// The rules documentation, in markdown. 26 | pub doc: String, 27 | } 28 | 29 | #[derive(Debug)] 30 | /// Stores analysis information for a document. 31 | pub struct Analysis { 32 | /// The URL of the document that this analysis is for. 33 | pub doc_url: Url, 34 | /// Holds analyses for individual rules. 35 | /// [RuleAnalysis] is [None] for builtins. 36 | pub rules: HashMap>, 37 | } 38 | 39 | impl Analysis { 40 | /// Updates this analysis from the given pairs. 41 | pub fn update_from(&mut self, pairs: Pairs) { 42 | self.rules = HashMap::new(); 43 | 44 | for builtin in BUILTINS.iter() { 45 | self.rules.insert(builtin.to_string(), None); 46 | } 47 | 48 | let mut preceding_docs = Vec::new(); 49 | let mut current_span: Option; 50 | 51 | for pair in pairs.clone() { 52 | if pair.as_rule() == Rule::grammar_rule { 53 | current_span = Some(pair.as_span()); 54 | let mut inner_pairs = pair.into_inner(); 55 | let inner = inner_pairs.next().unwrap(); 56 | 57 | match inner.as_rule() { 58 | Rule::line_doc => { 59 | preceding_docs.push(inner.into_inner().next().unwrap().as_str()); 60 | } 61 | Rule::identifier => { 62 | let expression_pair = inner_pairs 63 | .find(|r| r.as_rule() == Rule::expression) 64 | .expect("rule should contain expression"); 65 | let expression = expression_pair.as_str().to_owned(); 66 | let expressions = expression_pair 67 | .into_inner() 68 | .map(|e| { 69 | ( 70 | e.as_str().to_owned(), 71 | e.as_span().into_location(&self.doc_url), 72 | ) 73 | }) 74 | .collect(); 75 | let occurrences = pairs.find_occurrences(&self.doc_url, inner.as_str()); 76 | let mut docs = None; 77 | 78 | if !preceding_docs.is_empty() { 79 | let mut buf = String::new(); 80 | 81 | if preceding_docs.len() == 1 { 82 | buf.push_str(preceding_docs.first().unwrap()); 83 | } else { 84 | buf.push_str("- "); 85 | buf.push_str(preceding_docs.join("\n- ").as_str()); 86 | } 87 | 88 | docs = Some(buf); 89 | preceding_docs.clear(); 90 | } 91 | 92 | self.rules.insert( 93 | inner.as_str().to_owned(), 94 | Some(RuleAnalysis { 95 | identifier_location: inner.as_span().into_location(&self.doc_url), 96 | definition_location: current_span 97 | .expect("rule should have a defined span") 98 | .into_location(&self.doc_url), 99 | tokens: expressions, 100 | expression, 101 | occurrences, 102 | doc: docs.unwrap_or_else(|| "".to_owned()), 103 | }), 104 | ); 105 | } 106 | _ => {} 107 | } 108 | } 109 | } 110 | } 111 | 112 | pub fn get_unused_rules(&self) -> Vec<(&String, &Location)> { 113 | self.rules 114 | .iter() 115 | .filter(|(_, ra)| { 116 | if let Some(ra) = ra { 117 | ra.occurrences.len() == 1 118 | } else { 119 | false 120 | } 121 | }) 122 | .filter(|(name, _)| !BUILTINS.contains(&name.as_str()) && !name.starts_with('_')) 123 | .map(|(name, ra)| { 124 | ( 125 | name, 126 | ra.as_ref().unwrap().occurrences.first().unwrap_or_else(|| { 127 | panic!("Expected at least one occurrence for rule {}", name) 128 | }), 129 | ) 130 | }) 131 | .collect() 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /language-server/src/builtins.rs: -------------------------------------------------------------------------------- 1 | pub const BUILTINS: &[&str] = &[ 2 | "ANY", 3 | "WHITESPACE", 4 | "COMMENT", 5 | "SOI", 6 | "EOI", 7 | "ASCII_DIGIT", 8 | "ASCII_NONZERO_DIGIT", 9 | "ASCII_BIN_DIGIT", 10 | "ASCII_OCT_DIGIT", 11 | "ASCII_HEX_DIGIT", 12 | "ASCII_ALPHA_LOWER", 13 | "ASCII_ALPHA_UPPER", 14 | "ASCII_ALPHA", 15 | "ASCII_ALPHANUMERIC", 16 | "NEWLINE", 17 | "LETTER", 18 | "CASED_LETTER", 19 | "UPPERCASE_LETTER", 20 | "LOWERCASE_LETTER", 21 | "TITLECASE_LETTER", 22 | "MODIFIER_LETTER", 23 | "OTHER_LETTER", 24 | "MARK", 25 | "NON_SPACING_MARK", 26 | "SPACING_MARK", 27 | "ENCLOSING_MARK", 28 | "NUMBER", 29 | "DECIMAL_NUMBER", 30 | "LETTER_NUMBER", 31 | "OTHER_NUMBER", 32 | "PUNCTUATION", 33 | "CONNECTOR_PUNCTUATION", 34 | "DASH_PUNCTUATION", 35 | "OPEN_PUNCTUATION", 36 | "CLOSE_PUNCTUATION", 37 | "INITIAL_PUNCTUATION", 38 | "FINAL_PUNCTUATION", 39 | "OTHER_PUNCTUATION", 40 | "SYMBOL", 41 | "MATH_SYMBOL", 42 | "CURRENCY_SYMBOL", 43 | "MODIFIER_SYMBOL", 44 | "OTHER_SYMBOL", 45 | "SEPARATOR", 46 | "SPACE_SEPARATOR", 47 | "LINE_SEPARATOR", 48 | "PARAGRAPH_SEPARATOR", 49 | "CONTROL", 50 | "FORMAT", 51 | "PRIVATE_USE", 52 | "SURROGATE", 53 | "UNASSIGNED", 54 | "ALPHABETIC", 55 | "BIDI_CONTROL", 56 | "BIDI_MIRRORED", 57 | "CASE_IGNORABLE", 58 | "CASED", 59 | "CHANGES_WHEN_CASEFOLDED", 60 | "CHANGES_WHEN_CASEMAPPED", 61 | "CHANGES_WHEN_LOWERCASED", 62 | "CHANGES_WHEN_TITLECASED", 63 | "CHANGES_WHEN_UPPERCASED", 64 | "DASH", 65 | "DEFAULT_IGNORABLE_CODE_POINT", 66 | "DEPRECATED", 67 | "DIACRITIC", 68 | "EMOJI", 69 | "EMOJI_COMPONENT", 70 | "EMOJI_MODIFIER", 71 | "EMOJI_MODIFIER_BASE", 72 | "EMOJI_PRESENTATION", 73 | "EXTENDER", 74 | "GRAPHEME_BASE", 75 | "GRAPHEME_EXTEND", 76 | "GRAPHEME_LINK", 77 | "HEX_DIGIT", 78 | "HYPHEN", 79 | "IDS_BINARY_OPERATOR", 80 | "IDS_TRINARY_OPERATOR", 81 | "ID_CONTINUE", 82 | "ID_START", 83 | "IDEOGRAPHIC", 84 | "JOIN_CONTROL", 85 | "LOGICAL_ORDER_EXCEPTION", 86 | "LOWERCASE", 87 | "MATH", 88 | "NONCHARACTER_CODE_POINT", 89 | "OTHER_ALPHABETIC", 90 | "OTHER_DEFAULT_IGNORABLE_CODE_POINT", 91 | "OTHER_GRAPHEME_EXTEND", 92 | "OTHER_ID_CONTINUE", 93 | "OTHER_ID_START", 94 | "OTHER_LOWERCASE", 95 | "OTHER_MATH", 96 | "OTHER_UPPERCASE", 97 | "PATTERN_SYNTAX", 98 | "PATTERN_WHITE_SPACE", 99 | "PREPENDED_CONCATENATION_MARK", 100 | "QUOTATION_MARK", 101 | "RADICAL", 102 | "REGIONAL_INDICATOR", 103 | "SENTENCE_TERMINAL", 104 | "SOFT_DOTTED", 105 | "TERMINAL_PUNCTUATION", 106 | "UNIFIED_IDEOGRAPH", 107 | "UPPERCASE", 108 | "VARIATION_SELECTOR", 109 | "WHITE_SPACE", 110 | "XID_CONTINUE", 111 | "XID_START", 112 | "ADLAM", 113 | "AHOM", 114 | "ANATOLIAN_HIEROGLYPHS", 115 | "ARABIC", 116 | "ARMENIAN", 117 | "AVESTAN", 118 | "BALINESE", 119 | "BAMUM", 120 | "BASSA_VAH", 121 | "BATAK", 122 | "BENGALI", 123 | "BHAIKSUKI", 124 | "BOPOMOFO", 125 | "BRAHMI", 126 | "BRAILLE", 127 | "BUGINESE", 128 | "BUHID", 129 | "CANADIAN_ABORIGINAL", 130 | "CARIAN", 131 | "CAUCASIAN_ALBANIAN", 132 | "CHAKMA", 133 | "CHAM", 134 | "CHEROKEE", 135 | "CHORASMIAN", 136 | "COMMON", 137 | "COPTIC", 138 | "CUNEIFORM", 139 | "CYPRIOT", 140 | "CYPRO_MINOAN", 141 | "CYRILLIC", 142 | "DESERET", 143 | "DEVANAGARI", 144 | "DIVES_AKURU", 145 | "DOGRA", 146 | "DUPLOYAN", 147 | "EGYPTIAN_HIEROGLYPHS", 148 | "ELBASAN", 149 | "ELYMAIC", 150 | "ETHIOPIC", 151 | "GEORGIAN", 152 | "GLAGOLITIC", 153 | "GOTHIC", 154 | "GRANTHA", 155 | "GREEK", 156 | "GUJARATI", 157 | "GUNJALA_GONDI", 158 | "GURMUKHI", 159 | "HAN", 160 | "HANGUL", 161 | "HANIFI_ROHINGYA", 162 | "HANUNOO", 163 | "HATRAN", 164 | "HEBREW", 165 | "HIRAGANA", 166 | "IMPERIAL_ARAMAIC", 167 | "INHERITED", 168 | "INSCRIPTIONAL_PAHLAVI", 169 | "INSCRIPTIONAL_PARTHIAN", 170 | "JAVANESE", 171 | "KAITHI", 172 | "KANNADA", 173 | "KATAKANA", 174 | "KAWI", 175 | "KAYAH_LI", 176 | "KHAROSHTHI", 177 | "KHITAN_SMALL_SCRIPT", 178 | "KHMER", 179 | "KHOJKI", 180 | "KHUDAWADI", 181 | "LAO", 182 | "LATIN", 183 | "LEPCHA", 184 | "LIMBU", 185 | "LINEAR_A", 186 | "LINEAR_B", 187 | "LISU", 188 | "LYCIAN", 189 | "LYDIAN", 190 | "MAHAJANI", 191 | "MAKASAR", 192 | "MALAYALAM", 193 | "MANDAIC", 194 | "MANICHAEAN", 195 | "MARCHEN", 196 | "MASARAM_GONDI", 197 | "MEDEFAIDRIN", 198 | "MEETEI_MAYEK", 199 | "MENDE_KIKAKUI", 200 | "MEROITIC_CURSIVE", 201 | "MEROITIC_HIEROGLYPHS", 202 | "MIAO", 203 | "MODI", 204 | "MONGOLIAN", 205 | "MRO", 206 | "MULTANI", 207 | "MYANMAR", 208 | "NABATAEAN", 209 | "NAG_MUNDARI", 210 | "NANDINAGARI", 211 | "NEW_TAI_LUE", 212 | "NEWA", 213 | "NKO", 214 | "NUSHU", 215 | "NYIAKENG_PUACHUE_HMONG", 216 | "OGHAM", 217 | "OL_CHIKI", 218 | "OLD_HUNGARIAN", 219 | "OLD_ITALIC", 220 | "OLD_NORTH_ARABIAN", 221 | "OLD_PERMIC", 222 | "OLD_PERSIAN", 223 | "OLD_SOGDIAN", 224 | "OLD_SOUTH_ARABIAN", 225 | "OLD_TURKIC", 226 | "OLD_UYGHUR", 227 | "ORIYA", 228 | "OSAGE", 229 | "OSMANYA", 230 | "PAHAWH_HMONG", 231 | "PALMYRENE", 232 | "PAU_CIN_HAU", 233 | "PHAGS_PA", 234 | "PHOENICIAN", 235 | "PSALTER_PAHLAVI", 236 | "REJANG", 237 | "RUNIC", 238 | "SAMARITAN", 239 | "SAURASHTRA", 240 | "SHARADA", 241 | "SHAVIAN", 242 | "SIDDHAM", 243 | "SIGNWRITING", 244 | "SINHALA", 245 | "SOGDIAN", 246 | "SORA_SOMPENG", 247 | "SOYOMBO", 248 | "SUNDANESE", 249 | "SYLOTI_NAGRI", 250 | "SYRIAC", 251 | "TAGALOG", 252 | "TAGBANWA", 253 | "TAI_LE", 254 | "TAI_THAM", 255 | "TAI_VIET", 256 | "TAKRI", 257 | "TAMIL", 258 | "TANGSA", 259 | "TANGUT", 260 | "TELUGU", 261 | "THAANA", 262 | "THAI", 263 | "TIBETAN", 264 | "TIFINAGH", 265 | "TIRHUTA", 266 | "TOTO", 267 | "UGARITIC", 268 | "VAI", 269 | "VITHKUQI", 270 | "WANCHO", 271 | "WARANG_CITI", 272 | "YEZIDI", 273 | "YI", 274 | "ZANABAZAR_SQUARE", 275 | ]; 276 | 277 | pub fn get_builtin_description(rule: &str) -> Option<&str> { 278 | match rule { 279 | "ANY" => Some("Matches any character."), 280 | "SOI" => Some("Matches the start of the input. Does not consume any characters."), 281 | "EOI" => Some("Matches the end of the input. Does not consume any characters."), 282 | 283 | "ASCII_DIGIT" => Some("Matches any ASCII digit (0-9)."), 284 | "ASCII_NONZERO_DIGIT" => Some("Matches any non-zero ASCII digit (1-9)."), 285 | "ASCII_BIN_DIGIT" => Some("Matches any ASCII binary digit (0-1)."), 286 | "ASCII_OCT_DIGIT" => Some("Matches any ASCII octal digit (0-7)."), 287 | "ASCII_HEX_DIGIT" => Some("Matches any ASCII hexadecimal digit (0-9, a-f, A-F)."), 288 | 289 | "ASCII_ALPHA_LOWER" => Some("Matches any ASCII lowercase letter (a-z)."), 290 | "ASCII_ALPHA_UPPER" => Some("Matches any ASCII uppercase letter (A-Z)."), 291 | "ASCII_ALPHA" => Some("Matches any ASCII letter (a-z, A-Z)."), 292 | 293 | "ASCII_ALPHANUMERIC" => Some("Matches any ASCII alphanumeric character (0-9, a-z, A-Z)."), 294 | "NEWLINE" => Some("Matches any newline character (\\n, \\r\\n, \\r)."), 295 | 296 | "LETTER" => Some("Matches any Unicode letter."), 297 | "CASED_LETTER" => Some("Matches any upper or lower case Unicode letter."), 298 | "UPPERCASE_LETTER" => Some("Matches any uppercase Unicode letter."), 299 | "LOWERCASE_LETTER" => Some("Matches any lowercase Unicode letter."), 300 | "TITLECASE_LETTER" => Some("Matches any titlecase Unicode letter."), 301 | "MODIFIER_LETTER" => Some("Matches any Unicode modifier letter."), 302 | "OTHER_LETTER" => { 303 | Some("Matches any Unicode letter that does not fit into any other defined categories.") 304 | } 305 | 306 | "MARK" => Some("Matches any Unicode mark."), 307 | "NON_SPACING_MARK" => Some("Matches any Unicode non-spacing mark."), 308 | "SPACING_MARK" => Some("Matches any Unicode spacing mark."), 309 | "ENCLOSING_MARK" => Some("Matches any Unicode enclosing mark."), 310 | 311 | "NUMBER" => Some("Matches any Unicode number."), 312 | "DECIMAL_NUMBER" => Some("Matches any Unicode decimal number."), 313 | "LETTER_NUMBER" => Some("Matches any Unicode letter number."), 314 | "OTHER_NUMBER" => { 315 | Some("Matches any Unicode number that does not fit into any other defined categories.") 316 | } 317 | 318 | "PUNCTUATION" => Some("Matches any Unicode punctuation."), 319 | "CONNECTOR_PUNCTUATION" => Some("Matches any Unicode connector punctuation."), 320 | "DASH_PUNCTUATION" => Some("Matches any Unicode dash punctuation."), 321 | "OPEN_PUNCTUATION" => Some("Matches any Unicode open punctuation."), 322 | "CLOSE_PUNCTUATION" => Some("Matches any Unicode close punctuation."), 323 | "INITIAL_PUNCTUATION" => Some("Matches any Unicode initial punctuation."), 324 | "FINAL_PUNCTUATION" => Some("Matches any Unicode final punctuation."), 325 | "OTHER_PUNCTUATION" => Some( 326 | "Matches any Unicode punctuation that does not fit into any other defined categories.", 327 | ), 328 | 329 | "SYMBOL" => Some("Matches any Unicode symbol."), 330 | "MATH_SYMBOL" => Some("Matches any Unicode math symbol."), 331 | "CURRENCY_SYMBOL" => Some("Matches any Unicode currency symbol."), 332 | "MODIFIER_SYMBOL" => Some("Matches any Unicode modifier symbol."), 333 | "OTHER_SYMBOL" => { 334 | Some("Matches any Unicode symbol that does not fit into any other defined categories.") 335 | } 336 | 337 | "SEPARATOR" => Some("Matches any Unicode separator."), 338 | "SPACE_SEPARATOR" => Some("Matches any Unicode space separator."), 339 | "LINE_SEPARATOR" => Some("Matches any Unicode line separator."), 340 | "PARAGRAPH_SEPARATOR" => Some("Matches any Unicode paragraph separator."), 341 | 342 | "OTHER" => Some( 343 | "Matches any Unicode character that does not fit into any other defined categories.", 344 | ), 345 | "CONTROL" => Some("Matches any Unicode control character."), 346 | "FORMAT" => Some("Matches any Unicode format character."), 347 | "SURROGATE" => Some("Matches any Unicode surrogate."), 348 | "PRIVATE_USE" => Some("Matches any Unicode private use character."), 349 | "UNASSIGNED" => Some("Matches any Unicode unassigned character."), 350 | 351 | "ALPHABETIC" => Some("Matches any Unicode alphabetic character."), 352 | "BIDI_CONTROL" => Some("Matches any Unicode bidirectional control character."), 353 | "BIDI_MIRRORED" => Some("Matches any Unicode bidirectional mirrored character."), 354 | "CASE_IGNORABLE" => Some("Matches any Unicode case-ignorable character."), 355 | "CASED" => Some("Matches any Unicode cased character."), 356 | "CHANGES_WHEN_CASEFOLDED" => { 357 | Some("Matches any Unicode character that changes when casefolded.") 358 | } 359 | "CHANGES_WHEN_CASEMAPPED" => { 360 | Some("Matches any Unicode character that changes when casemapped.") 361 | } 362 | "CHANGES_WHEN_LOWERCASED" => { 363 | Some("Matches any Unicode character that changes when lowercased.") 364 | } 365 | "CHANGES_WHEN_TITLECASED" => { 366 | Some("Matches any Unicode character that changes when titlecased.") 367 | } 368 | "CHANGES_WHEN_UPPERCASED" => { 369 | Some("Matches any Unicode character that changes when uppercased.") 370 | } 371 | "DASH" => Some("Matches any Unicode dash character."), 372 | "DEFAULT_IGNORABLE_CODE_POINT" => Some("Matches any Unicode default-ignorable code point."), 373 | "DEPRECATED" => Some("Matches any Unicode deprecated character."), 374 | "DIACRITIC" => Some("Matches any Unicode diacritic character."), 375 | "EMOJI" => Some("Matches any Unicode emoji character."), 376 | "EMOJI_COMPONENT" => Some("Matches any Unicode emoji component character."), 377 | "EMOJI_MODIFIER" => Some("Matches any Unicode emoji modifier character."), 378 | "EMOJI_MODIFIER_BASE" => Some("Matches any Unicode emoji modifier base character."), 379 | "EMOJI_PRESENTATION" => Some("Matches any Unicode emoji presentation character."), 380 | "EXTENDED_PICTOGRAPHIC" => Some("Matches any Unicode extended pictographic character."), 381 | "EXTENDER" => Some("Matches any Unicode extender character."), 382 | "GRAPHEME_BASE" => Some("Matches any Unicode grapheme base character."), 383 | "GRAPHEME_EXTEND" => Some("Matches any Unicode grapheme extend character."), 384 | "GRAPHEME_LINK" => Some("Matches any Unicode grapheme link character."), 385 | "HEX_DIGIT" => Some("Matches any Unicode hexadecimal digit character."), 386 | "HYPHEN" => Some("Matches any Unicode hyphen character."), 387 | "IDS_BINARY_OPERATOR" => Some("Matches any Unicode IDS binary operator character."), 388 | "IDS_TRINARY_OPERATOR" => Some("Matches any Unicode IDS trinary operator character."), 389 | "ID_CONTINUE" => Some("Matches any Unicode ID continue character."), 390 | "ID_START" => Some("Matches any Unicode ID start character."), 391 | "IDEOGRAPHIC" => Some("Matches any Unicode ideographic character."), 392 | "JOIN_CONTROL" => Some("Matches any Unicode join control character."), 393 | "LOGICAL_ORDER_EXCEPTION" => Some("Matches any Unicode logical order exception character."), 394 | "LOWERCASE" => Some("Matches any Unicode lowercase character."), 395 | "MATH" => Some("Matches any Unicode math character."), 396 | "NONCHARACTER_CODE_POINT" => Some("Matches any Unicode noncharacter code point."), 397 | "OTHER_ALPHABETIC" => Some("Matches any Unicode other alphabetic character."), 398 | "OTHER_DEFAULT_IGNORABLE_CODE_POINT" => { 399 | Some("Matches any Unicode other default-ignorable code point.") 400 | } 401 | "OTHER_GRAPHEME_EXTEND" => Some("Matches any Unicode other grapheme extend character."), 402 | "OTHER_ID_CONTINUE" => Some("Matches any Unicode other ID continue character."), 403 | "OTHER_ID_START" => Some("Matches any Unicode other ID start character."), 404 | "OTHER_LOWERCASE" => Some("Matches any Unicode other lowercase character."), 405 | "OTHER_MATH" => Some("Matches any Unicode other math character."), 406 | "OTHER_UPPERCASE" => Some("Matches any Unicode other uppercase character."), 407 | "PATTERN_SYNTAX" => Some("Matches any Unicode pattern syntax character."), 408 | "PATTERN_WHITE_SPACE" => Some("Matches any Unicode pattern white space character."), 409 | "PREPENDED_CONCATENATION_MARK" => { 410 | Some("Matches any Unicode prepended concatenation mark character.") 411 | } 412 | "QUOTATION_MARK" => Some("Matches any Unicode quotation mark character."), 413 | "RADICAL" => Some("Matches any Unicode radical character."), 414 | "REGIONAL_INDICATOR" => Some("Matches any Unicode regional indicator character."), 415 | "SENTENCE_TERMINAL" => Some("Matches any Unicode sentence terminal character."), 416 | "SOFT_DOTTED" => Some("Matches any Unicode soft-dotted character."), 417 | "TERMINAL_PUNCTUATION" => Some("Matches any Unicode terminal punctuation character."), 418 | "UNIFIED_IDEOGRAPH" => Some("Matches any Unicode unified ideograph character."), 419 | "UPPERCASE" => Some("Matches any Unicode uppercase character."), 420 | "VARIATION_SELECTOR" => Some("Matches any Unicode variation selector character."), 421 | "WHITE_SPACE" => Some("Matches any Unicode white space character."), 422 | "XID_CONTINUE" => Some("Matches any Unicode XID continue character."), 423 | "XID_START" => Some("Matches any Unicode XID start character."), 424 | 425 | "ADLAM" => Some("Matches any Unicode Adlam character."), 426 | "AHOM" => Some("Matches any Unicode Ahom character."), 427 | "ANATOLIAN_HIEROGLYPHS" => Some("Matches any Unicode Anatolian Hieroglyphs character."), 428 | "ARABIC" => Some("Matches any Unicode Arabic character."), 429 | "ARMENIAN" => Some("Matches any Unicode Armenian character."), 430 | "AVESTAN" => Some("Matches any Unicode Avestan character."), 431 | "BALINESE" => Some("Matches any Unicode Balinese character."), 432 | "BAMUM" => Some("Matches any Unicode Bamum character."), 433 | "BASSA_VAH" => Some("Matches any Unicode Bassa Vah character."), 434 | "BATAK" => Some("Matches any Unicode Batak character."), 435 | "BENGALI" => Some("Matches any Unicode Bengali character."), 436 | "BHAIKSUKI" => Some("Matches any Unicode Bhaiksuki character."), 437 | "BOPOMOFO" => Some("Matches any Unicode Bopomofo character."), 438 | "BRAHMI" => Some("Matches any Unicode Brahmi character."), 439 | "BRAILLE" => Some("Matches any Unicode Braille character."), 440 | "BUGINESE" => Some("Matches any Unicode Buginese character."), 441 | "BUHID" => Some("Matches any Unicode Buhid character."), 442 | "CANADIAN_ABORIGINAL" => Some("Matches any Unicode Canadian Aboriginal character."), 443 | "CARIAN" => Some("Matches any Unicode Carian character."), 444 | "CAUCASIAN_ALBANIAN" => Some("Matches any Unicode Caucasian Albanian character."), 445 | "CHAKMA" => Some("Matches any Unicode Chakma character."), 446 | "CHAM" => Some("Matches any Unicode Cham character."), 447 | "CHEROKEE" => Some("Matches any Unicode Cherokee character."), 448 | "CHORASMIAN" => Some("Matches any Unicode Chorasmian character."), 449 | "COMMON" => Some("Matches any Unicode Common character."), 450 | "COPTIC" => Some("Matches any Unicode Coptic character."), 451 | "CUNEIFORM" => Some("Matches any Unicode Cuneiform character."), 452 | "CYPRIOT" => Some("Matches any Unicode Cypriot character."), 453 | "CYPRO_MINOAN" => Some("Matches any Unicode Cypro-Minoan character."), 454 | "CYRILLIC" => Some("Matches any Unicode Cyrillic character."), 455 | "DESERET" => Some("Matches any Unicode Deseret character."), 456 | "DEVANAGARI" => Some("Matches any Unicode Devanagari character."), 457 | "DIVES_AKURU" => Some("Matches any Unicode Dives Akuru character."), 458 | "DOGRA" => Some("Matches any Unicode Dogra character."), 459 | "DUPLOYAN" => Some("Matches any Unicode Duployan character."), 460 | "EGYPTIAN_HIEROGLYPHS" => Some("Matches any Unicode Egyptian Hieroglyphs character."), 461 | "ELBASAN" => Some("Matches any Unicode Elbasan character."), 462 | "ELYMAIC" => Some("Matches any Unicode Elymaic character."), 463 | "ETHIOPIC" => Some("Matches any Unicode Ethiopic character."), 464 | "GEORGIAN" => Some("Matches any Unicode Georgian character."), 465 | "GLAGOLITIC" => Some("Matches any Unicode Glagolitic character."), 466 | "GOTHIC" => Some("Matches any Unicode Gothic character."), 467 | "GRANTHA" => Some("Matches any Unicode Grantha character."), 468 | "GREEK" => Some("Matches any Unicode Greek character."), 469 | "GUJARATI" => Some("Matches any Unicode Gujarati character."), 470 | "GUNJALA_GONDI" => Some("Matches any Unicode Gunjala Gondi character."), 471 | "GURMUKHI" => Some("Matches any Unicode Gurmukhi character."), 472 | "HAN" => Some("Matches any Unicode Han character."), 473 | "HANGUL" => Some("Matches any Unicode Hangul character."), 474 | "HANIFI_ROHINGYA" => Some("Matches any Unicode Hanifi Rohingya character."), 475 | "HANUNOO" => Some("Matches any Unicode Hanunoo character."), 476 | "HATRAN" => Some("Matches any Unicode Hatran character."), 477 | "HEBREW" => Some("Matches any Unicode Hebrew character."), 478 | "HIRAGANA" => Some("Matches any Unicode Hiragana character."), 479 | "IMPERIAL_ARAMAIC" => Some("Matches any Unicode Imperial Aramaic character."), 480 | "INHERITED" => Some("Matches any Unicode Inherited character."), 481 | "INSCRIPTIONAL_PAHLAVI" => Some("Matches any Unicode Inscriptional Pahlavi character."), 482 | "INSCRIPTIONAL_PARTHIAN" => Some("Matches any Unicode Inscriptional Parthian character."), 483 | "JAVANESE" => Some("Matches any Unicode Javanese character."), 484 | "KAITHI" => Some("Matches any Unicode Kaithi character."), 485 | "KANNADA" => Some("Matches any Unicode Kannada character."), 486 | "KATAKANA" => Some("Matches any Unicode Katakana character."), 487 | "KAWI" => Some("Matches any Unicode Kawi character."), 488 | "KAYAH_LI" => Some("Matches any Unicode Kayah Li character."), 489 | "KHAROSHTHI" => Some("Matches any Unicode Kharoshthi character."), 490 | "KHITAN_SMALL_SCRIPT" => Some("Matches any Unicode Khitan Small Script character."), 491 | "KHMER" => Some("Matches any Unicode Khmer character."), 492 | "KHOJKI" => Some("Matches any Unicode Khojki character."), 493 | "KHUDAWADI" => Some("Matches any Unicode Khudawadi character."), 494 | "LAO" => Some("Matches any Unicode Lao character."), 495 | "LATIN" => Some("Matches any Unicode Latin character."), 496 | "LEPCHA" => Some("Matches any Unicode Lepcha character."), 497 | "LIMBU" => Some("Matches any Unicode Limbu character."), 498 | "LINEAR_A" => Some("Matches any Unicode Linear A character."), 499 | "LINEAR_B" => Some("Matches any Unicode Linear B character."), 500 | "LISU" => Some("Matches any Unicode Lisu character."), 501 | "LYCIAN" => Some("Matches any Unicode Lycian character."), 502 | "LYDIAN" => Some("Matches any Unicode Lydian character."), 503 | "MAHAJANI" => Some("Matches any Unicode Mahajani character."), 504 | "MAKASAR" => Some("Matches any Unicode Makasar character."), 505 | "MALAYALAM" => Some("Matches any Unicode Malayalam character."), 506 | "MANDAIC" => Some("Matches any Unicode Mandaic character."), 507 | "MANICHAEAN" => Some("Matches any Unicode Manichaean character."), 508 | "MARCHEN" => Some("Matches any Unicode Marchen character."), 509 | "MASARAM_GONDI" => Some("Matches any Unicode Masaram Gondi character."), 510 | "MEDEFAIDRIN" => Some("Matches any Unicode Medefaidrin character."), 511 | "MEETEI_MAYEK" => Some("Matches any Unicode Meetei Mayek character."), 512 | "MENDE_KIKAKUI" => Some("Matches any Unicode Mende Kikakui character."), 513 | "MEROITIC_CURSIVE" => Some("Matches any Unicode Meroitic Cursive character."), 514 | "MEROITIC_HIEROGLYPHS" => Some("Matches any Unicode Meroitic Hieroglyphs character."), 515 | "MIAO" => Some("Matches any Unicode Miao character."), 516 | "MODI" => Some("Matches any Unicode Modi character."), 517 | "MONGOLIAN" => Some("Matches any Unicode Mongolian character."), 518 | "MRO" => Some("Matches any Unicode Mro character."), 519 | "MULTANI" => Some("Matches any Unicode Multani character."), 520 | "MYANMAR" => Some("Matches any Unicode Myanmar character."), 521 | "NABATAEAN" => Some("Matches any Unicode Nabataean character."), 522 | "NAG_MUNDARI" => Some("Matches any Unicode Nag Mundari character."), 523 | "NANDINAGARI" => Some("Matches any Unicode Nandinagari character."), 524 | "NEW_TAI_LUE" => Some("Matches any Unicode New Tai Lue character."), 525 | "NEWA" => Some("Matches any Unicode Newa character."), 526 | "NKO" => Some("Matches any Unicode Nko character."), 527 | "NUSHU" => Some("Matches any Unicode Nushu character."), 528 | "NYIAKENG_PUACHUE_HMONG" => Some("Matches any Unicode Nyiakeng Puachue Hmong character."), 529 | "OGHAM" => Some("Matches any Unicode Ogham character."), 530 | "OL_CHIKI" => Some("Matches any Unicode Ol Chiki character."), 531 | "OLD_HUNGARIAN" => Some("Matches any Unicode Old Hungarian character."), 532 | "OLD_ITALIC" => Some("Matches any Unicode Old Italic character."), 533 | "OLD_NORTH_ARABIAN" => Some("Matches any Unicode Old North Arabian character."), 534 | "OLD_PERMIC" => Some("Matches any Unicode Old Permic character."), 535 | "OLD_PERSIAN" => Some("Matches any Unicode Old Persian character."), 536 | "OLD_SOGDIAN" => Some("Matches any Unicode Old Sogdian character."), 537 | "OLD_SOUTH_ARABIAN" => Some("Matches any Unicode Old South Arabian character."), 538 | "OLD_TURKIC" => Some("Matches any Unicode Old Turkic character."), 539 | "OLD_UYGHUR" => Some("Matches any Unicode Old Uyghur character."), 540 | "ORIYA" => Some("Matches any Unicode Oriya character."), 541 | "OSAGE" => Some("Matches any Unicode Osage character."), 542 | "OSMANYA" => Some("Matches any Unicode Osmanya character."), 543 | "PAHAWH_HMONG" => Some("Matches any Unicode Pahawh Hmong character."), 544 | "PALMYRENE" => Some("Matches any Unicode Palmyrene character."), 545 | "PAU_CIN_HAU" => Some("Matches any Unicode Pau Cin Hau character."), 546 | "PHAGS_PA" => Some("Matches any Unicode Phags Pa character."), 547 | "PHOENICIAN" => Some("Matches any Unicode Phoenician character."), 548 | "PSALTER_PAHLAVI" => Some("Matches any Unicode Psalter Pahlavi character."), 549 | "REJANG" => Some("Matches any Unicode Rejang character."), 550 | "RUNIC" => Some("Matches any Unicode Runic character."), 551 | "SAMARITAN" => Some("Matches any Unicode Samaritan character."), 552 | "SAURASHTRA" => Some("Matches any Unicode Saurashtra character."), 553 | "SHARADA" => Some("Matches any Unicode Sharada character."), 554 | "SHAVIAN" => Some("Matches any Unicode Shavian character."), 555 | "SIDDHAM" => Some("Matches any Unicode Siddham character."), 556 | "SIGNWRITING" => Some("Matches any Unicode SignWriting character."), 557 | "SINHALA" => Some("Matches any Unicode Sinhala character."), 558 | "SOGDIAN" => Some("Matches any Unicode Sogdian character."), 559 | "SORA_SOMPENG" => Some("Matches any Unicode Sora Sompeng character."), 560 | "SOYOMBO" => Some("Matches any Unicode Soyombo character."), 561 | "SUNDANESE" => Some("Matches any Unicode Sundanese character."), 562 | "SYLOTI_NAGRI" => Some("Matches any Unicode Syloti Nagri character."), 563 | "SYRIAC" => Some("Matches any Unicode Syriac character."), 564 | "TAGALOG" => Some("Matches any Unicode Tagalog character."), 565 | "TAGBANWA" => Some("Matches any Unicode Tagbanwa character."), 566 | "TAI_LE" => Some("Matches any Unicode Tai Le character."), 567 | "TAI_THAM" => Some("Matches any Unicode Tai Tham character."), 568 | "TAI_VIET" => Some("Matches any Unicode Tai Viet character."), 569 | "TAKRI" => Some("Matches any Unicode Takri character."), 570 | "TAMIL" => Some("Matches any Unicode Tamil character."), 571 | "TANGSA" => Some("Matches any Unicode Tangsa character."), 572 | "TANGUT" => Some("Matches any Unicode Tangut character."), 573 | "TELUGU" => Some("Matches any Unicode Telugu character."), 574 | "THAANA" => Some("Matches any Unicode Thaana character."), 575 | "THAI" => Some("Matches any Unicode Thai character."), 576 | "TIBETAN" => Some("Matches any Unicode Tibetan character."), 577 | "TIFINAGH" => Some("Matches any Unicode Tifinagh character."), 578 | "TIRHUTA" => Some("Matches any Unicode Tirhuta character."), 579 | "TOTO" => Some("Matches any Unicode Toto character."), 580 | "UGARITIC" => Some("Matches any Unicode Ugaritic character."), 581 | "VAI" => Some("Matches any Unicode Vai character."), 582 | "VITHKUQI" => Some("Matches any Unicode Vithkuqi character."), 583 | "WANCHO" => Some("Matches any Unicode Wancho character."), 584 | "WARANG_CITI" => Some("Matches any Unicode Warang Citi character."), 585 | "YEZIDI" => Some("Matches any Unicode Yezidi character."), 586 | "YI" => Some("Matches any Unicode Yi character."), 587 | "ZANABAZAR_SQUARE" => Some("Matches any Unicode Zanabazar Square character."), 588 | _ => None, 589 | } 590 | } 591 | -------------------------------------------------------------------------------- /language-server/src/capabilities.rs: -------------------------------------------------------------------------------- 1 | use tower_lsp::lsp_types::{ 2 | CodeActionKind, CodeActionOptions, CodeActionProviderCapability, CompletionOptions, 3 | FileOperationFilter, FileOperationPattern, FileOperationRegistrationOptions, 4 | HoverProviderCapability, InitializeResult, OneOf, ServerCapabilities, ServerInfo, 5 | TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions, 6 | WorkDoneProgressOptions, WorkspaceFileOperationsServerCapabilities, 7 | WorkspaceServerCapabilities, 8 | }; 9 | 10 | /// Returns the capabilities of the language server. 11 | pub fn capabilities() -> InitializeResult { 12 | InitializeResult { 13 | capabilities: ServerCapabilities { 14 | text_document_sync: Some(TextDocumentSyncCapability::Options( 15 | TextDocumentSyncOptions { 16 | change: Some(TextDocumentSyncKind::FULL), 17 | open_close: Some(true), 18 | ..Default::default() 19 | }, 20 | )), 21 | hover_provider: Some(HoverProviderCapability::Simple(true)), 22 | completion_provider: Some(CompletionOptions { 23 | trigger_characters: Some(vec!["{".to_string(), "~".to_string(), "|".to_string()]), 24 | ..Default::default() 25 | }), 26 | code_action_provider: Some(CodeActionProviderCapability::Options(CodeActionOptions { 27 | code_action_kinds: Some(vec![ 28 | CodeActionKind::REFACTOR_EXTRACT, 29 | CodeActionKind::REFACTOR_INLINE, 30 | ]), 31 | work_done_progress_options: WorkDoneProgressOptions::default(), 32 | resolve_provider: None, 33 | //FIXME(Jamalam): Use Default here once https://github.com/gluon-lang/lsp-types/issues/260 is resolved. 34 | // ..Default::default() 35 | })), 36 | definition_provider: Some(OneOf::Left(true)), 37 | references_provider: Some(OneOf::Left(true)), 38 | document_formatting_provider: Some(OneOf::Left(true)), 39 | rename_provider: Some(OneOf::Left(true)), 40 | workspace: Some(WorkspaceServerCapabilities { 41 | file_operations: Some(WorkspaceFileOperationsServerCapabilities { 42 | did_delete: Some(FileOperationRegistrationOptions { 43 | filters: vec![FileOperationFilter { 44 | pattern: FileOperationPattern { 45 | glob: "**".to_string(), 46 | ..Default::default() 47 | }, 48 | ..Default::default() 49 | }], 50 | }), 51 | ..Default::default() 52 | }), 53 | ..Default::default() 54 | }), 55 | ..Default::default() 56 | }, 57 | server_info: Some(ServerInfo { 58 | name: "Pest Language Server".to_string(), 59 | version: Some(env!("CARGO_PKG_VERSION").to_string()), 60 | }), 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /language-server/src/config.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | 3 | #[derive(Deserialize, Default, Debug)] 4 | #[serde(rename_all = "camelCase")] 5 | pub struct Config { 6 | pub always_used_rule_names: Vec, 7 | pub check_for_updates: bool, 8 | } 9 | -------------------------------------------------------------------------------- /language-server/src/helpers.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use pest::{ 4 | error::{ErrorVariant, LineColLocation}, 5 | iterators::Pairs, 6 | Span, 7 | }; 8 | use tower_lsp::lsp_types::{ 9 | Diagnostic, DiagnosticSeverity, Location, Position, PublishDiagnosticsParams, Range, 10 | TextDocumentItem, Url, 11 | }; 12 | 13 | use pest_meta::{ 14 | parser::{self, Rule}, 15 | validator, 16 | }; 17 | use unicode_segmentation::UnicodeSegmentation; 18 | 19 | pub type Documents = HashMap; 20 | pub type Diagnostics = HashMap; 21 | 22 | pub fn create_empty_diagnostics( 23 | (uri, doc): (&Url, &TextDocumentItem), 24 | ) -> (Url, PublishDiagnosticsParams) { 25 | let params = PublishDiagnosticsParams::new(uri.clone(), vec![], Some(doc.version)); 26 | (uri.clone(), params) 27 | } 28 | 29 | pub trait IntoRange { 30 | fn into_range(self) -> Range; 31 | } 32 | 33 | impl IntoRange for LineColLocation { 34 | fn into_range(self) -> Range { 35 | match self { 36 | LineColLocation::Pos((line, col)) => { 37 | let pos = Position::new(line as u32 - 1, col as u32 - 1); 38 | Range::new(pos, pos) 39 | } 40 | LineColLocation::Span((start_line, start_col), (end_line, end_col)) => Range::new( 41 | Position::new(start_line as u32 - 1, start_col as u32 - 1), 42 | Position::new(end_line as u32 - 1, end_col as u32 - 1), 43 | ), 44 | } 45 | } 46 | } 47 | 48 | impl IntoRange for Span<'_> { 49 | fn into_range(self) -> Range { 50 | let start = self.start_pos().line_col(); 51 | let end = self.end_pos().line_col(); 52 | LineColLocation::Span((start.0, start.1), (end.0, end.1)).into_range() 53 | } 54 | } 55 | 56 | pub trait IntoLocation { 57 | fn into_location(self, uri: &Url) -> Location; 58 | } 59 | 60 | impl IntoLocation for Span<'_> { 61 | fn into_location(self, uri: &Url) -> Location { 62 | Location::new(uri.clone(), self.into_range()) 63 | } 64 | } 65 | 66 | pub trait FindOccurrences<'a> { 67 | fn find_occurrences(&self, doc_uri: &Url, identifier: &'a str) -> Vec; 68 | } 69 | 70 | impl<'a> FindOccurrences<'a> for Pairs<'a, parser::Rule> { 71 | fn find_occurrences(&self, doc_uri: &Url, identifier: &'a str) -> Vec { 72 | let mut locs = vec![]; 73 | 74 | for pair in self.clone() { 75 | if pair.as_rule() == parser::Rule::identifier && pair.as_str() == identifier { 76 | locs.push(pair.as_span().into_location(doc_uri)); 77 | } 78 | 79 | let inner = pair.into_inner(); 80 | locs.extend(inner.find_occurrences(doc_uri, identifier)); 81 | } 82 | 83 | locs 84 | } 85 | } 86 | 87 | pub trait IntoRangeWithLine { 88 | fn into_range(self, line: u32) -> Range; 89 | } 90 | 91 | impl IntoRangeWithLine for std::ops::Range { 92 | fn into_range(self, line: u32) -> Range { 93 | let start = Position::new(line, self.start as u32); 94 | let end = Position::new(line, self.end as u32); 95 | Range::new(start, end) 96 | } 97 | } 98 | 99 | pub trait FindWordRange { 100 | fn get_word_range_at_idx(self, idx: usize) -> std::ops::Range; 101 | } 102 | 103 | impl FindWordRange for &str { 104 | fn get_word_range_at_idx(self, search_idx: usize) -> std::ops::Range { 105 | fn is_identifier(c: char) -> bool { 106 | !(c.is_whitespace() 107 | || c == '*' 108 | || c == '+' 109 | || c == '?' 110 | || c == '!' 111 | || c == '&' 112 | || c == '~' 113 | || c == '{' 114 | || c == '}' 115 | || c == '[' 116 | || c == ']' 117 | || c == '(' 118 | || c == ')') 119 | } 120 | 121 | let next = str_range(self, &(search_idx..self.len())) 122 | .graphemes(true) 123 | .enumerate() 124 | .find(|(_index, char)| !is_identifier(char.chars().next().unwrap_or(' '))) 125 | .map(|(index, _char)| index) 126 | .map(|index| search_idx + index) 127 | .unwrap_or(self.len()); 128 | 129 | let preceding = str_range(self, &(0..search_idx)) 130 | .graphemes(true) 131 | .rev() 132 | .enumerate() 133 | .find(|(_index, char)| !is_identifier(char.chars().next().unwrap_or(' '))) 134 | .map(|(index, _char)| index) 135 | .map(|index| search_idx - index) 136 | .unwrap_or(0); 137 | 138 | preceding..next 139 | } 140 | } 141 | 142 | /// Returns a string from a range of human characters (graphemes). Respects unicode. 143 | pub fn str_range(s: &str, range: &std::ops::Range) -> String { 144 | s.graphemes(true) 145 | .skip(range.start) 146 | .take(range.len()) 147 | .collect() 148 | } 149 | 150 | pub trait IntoDiagnostics { 151 | fn into_diagnostics(self) -> Vec; 152 | } 153 | 154 | impl IntoDiagnostics for Vec> { 155 | fn into_diagnostics(self) -> Vec { 156 | self.iter() 157 | .map(|e| { 158 | Diagnostic::new( 159 | e.line_col.clone().into_range(), 160 | Some(DiagnosticSeverity::ERROR), 161 | None, 162 | Some("Pest Language Server".to_owned()), 163 | match &e.variant { 164 | ErrorVariant::ParsingError { 165 | positives, 166 | negatives, 167 | } => { 168 | let mut message = "Parsing error".to_owned(); 169 | if !positives.is_empty() { 170 | message.push_str(" (expected "); 171 | message.push_str( 172 | positives 173 | .iter() 174 | .map(|s| format!("\"{:#?}\"", s)) 175 | .collect::>() 176 | .join(", ") 177 | .as_str(), 178 | ); 179 | message.push(')'); 180 | } 181 | 182 | if !negatives.is_empty() { 183 | message.push_str(" (unexpected "); 184 | message.push_str( 185 | negatives 186 | .iter() 187 | .map(|s| format!("\"{:#?}\"", s)) 188 | .collect::>() 189 | .join(", ") 190 | .as_str(), 191 | ); 192 | message.push(')'); 193 | } 194 | 195 | message 196 | } 197 | ErrorVariant::CustomError { message } => { 198 | let mut c = message.chars(); 199 | match c.next() { 200 | None => String::new(), 201 | Some(f) => f.to_uppercase().collect::() + c.as_str(), 202 | } 203 | } 204 | }, 205 | None, 206 | None, 207 | ) 208 | }) 209 | .collect() 210 | } 211 | } 212 | 213 | pub fn validate_pairs(pairs: Pairs<'_, Rule>) -> Result<(), Vec>> { 214 | validator::validate_pairs(pairs.clone())?; 215 | // This calls validator::validate_ast under the hood 216 | parser::consume_rules(pairs)?; 217 | Ok(()) 218 | } 219 | 220 | pub fn range_contains(primary: &Range, secondary: &Range) -> bool { 221 | primary.start.line <= secondary.start.line 222 | && primary.start.character <= secondary.start.character 223 | && primary.end.line >= secondary.end.line 224 | && primary.end.character >= secondary.end.character 225 | } 226 | -------------------------------------------------------------------------------- /language-server/src/lsp.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, str::Split}; 2 | 3 | use pest_meta::parser; 4 | use tower_lsp::{ 5 | jsonrpc::Result, 6 | lsp_types::{ 7 | request::{GotoDeclarationParams, GotoDeclarationResponse}, 8 | CodeAction, CodeActionKind, CodeActionOrCommand, CodeActionParams, CodeActionResponse, 9 | CompletionItem, CompletionItemKind, CompletionParams, CompletionResponse, 10 | ConfigurationItem, DeleteFilesParams, Diagnostic, DiagnosticSeverity, 11 | DidChangeConfigurationParams, DidChangeTextDocumentParams, DidChangeWatchedFilesParams, 12 | DidOpenTextDocumentParams, DocumentChanges, DocumentFormattingParams, FileChangeType, 13 | FileDelete, FileEvent, GotoDefinitionParams, GotoDefinitionResponse, Hover, HoverContents, 14 | HoverParams, InitializedParams, Location, MarkedString, MessageType, OneOf, 15 | OptionalVersionedTextDocumentIdentifier, Position, PublishDiagnosticsParams, Range, 16 | ReferenceParams, RenameParams, TextDocumentEdit, TextDocumentItem, TextEdit, Url, 17 | VersionedTextDocumentIdentifier, WorkspaceEdit, 18 | }, 19 | Client, 20 | }; 21 | use unicode_segmentation::UnicodeSegmentation; 22 | 23 | use crate::{ 24 | analysis::{Analysis, RuleAnalysis}, 25 | builtins::BUILTINS, 26 | config::Config, 27 | helpers::{ 28 | create_empty_diagnostics, range_contains, str_range, validate_pairs, Diagnostics, 29 | Documents, FindWordRange, IntoDiagnostics, IntoRangeWithLine, 30 | }, 31 | }; 32 | use crate::{builtins::get_builtin_description, update_checker::check_for_updates}; 33 | 34 | #[derive(Debug)] 35 | pub struct PestLanguageServerImpl { 36 | pub client: Client, 37 | pub documents: Documents, 38 | pub analyses: HashMap, 39 | pub config: Config, 40 | } 41 | 42 | impl PestLanguageServerImpl { 43 | pub async fn initialized(&mut self, _: InitializedParams) { 44 | let config_items = self 45 | .client 46 | .configuration(vec![ConfigurationItem { 47 | scope_uri: None, 48 | section: Some("pestIdeTools".to_string()), 49 | }]) 50 | .await; 51 | 52 | let mut updated_config = false; 53 | 54 | if let Ok(config_items) = config_items { 55 | if let Some(config) = config_items.into_iter().next() { 56 | if let Ok(config) = serde_json::from_value(config) { 57 | self.config = config; 58 | updated_config = true; 59 | } 60 | } 61 | } 62 | 63 | if !updated_config { 64 | self.client 65 | .log_message( 66 | MessageType::ERROR, 67 | "Failed to retrieve configuration from client.", 68 | ) 69 | .await; 70 | } 71 | 72 | self.client 73 | .log_message( 74 | MessageType::INFO, 75 | format!("Pest Language Server v{}", env!("CARGO_PKG_VERSION")), 76 | ) 77 | .await; 78 | 79 | if self.config.check_for_updates { 80 | self.client 81 | .log_message(MessageType::INFO, "Checking for updates...".to_string()) 82 | .await; 83 | 84 | if let Some(new_version) = check_for_updates().await { 85 | self.client 86 | .show_message( 87 | MessageType::INFO, 88 | format!( 89 | "A new version of the Pest Language Server is available: v{}", 90 | new_version 91 | ), 92 | ) 93 | .await; 94 | } 95 | } 96 | } 97 | 98 | pub async fn shutdown(&self) -> Result<()> { 99 | self.client 100 | .log_message(MessageType::INFO, "Pest Language Server shutting down :)") 101 | .await; 102 | Ok(()) 103 | } 104 | 105 | pub async fn did_change_configuration(&mut self, params: DidChangeConfigurationParams) { 106 | if let Ok(config) = serde_json::from_value(params.settings) { 107 | self.config = config; 108 | self.client 109 | .log_message( 110 | MessageType::INFO, 111 | "Updated configuration from client.".to_string(), 112 | ) 113 | .await; 114 | 115 | let diagnostics = self.reload().await; 116 | self.send_diagnostics(diagnostics).await; 117 | } 118 | } 119 | 120 | pub async fn did_open(&mut self, params: DidOpenTextDocumentParams) { 121 | let DidOpenTextDocumentParams { text_document } = params; 122 | self.client 123 | .log_message(MessageType::INFO, format!("Opening {}", text_document.uri)) 124 | .await; 125 | 126 | if self.upsert_document(text_document).is_some() { 127 | self.client 128 | .log_message( 129 | MessageType::INFO, 130 | "\tReopened already tracked document.".to_string(), 131 | ) 132 | .await; 133 | } 134 | 135 | let diagnostics = self.reload().await; 136 | self.send_diagnostics(diagnostics).await; 137 | } 138 | 139 | pub async fn did_change(&mut self, params: DidChangeTextDocumentParams) { 140 | let DidChangeTextDocumentParams { 141 | text_document, 142 | content_changes, 143 | } = params; 144 | let VersionedTextDocumentIdentifier { uri, version } = text_document; 145 | 146 | assert_eq!(content_changes.len(), 1); 147 | let change = content_changes.into_iter().next().unwrap(); 148 | assert!(change.range.is_none()); 149 | 150 | let updated_doc = 151 | TextDocumentItem::new(uri.clone(), "pest".to_owned(), version, change.text); 152 | 153 | if self.upsert_document(updated_doc).is_none() { 154 | self.client 155 | .log_message( 156 | MessageType::INFO, 157 | format!("Updated untracked document {}", uri), 158 | ) 159 | .await; 160 | } 161 | 162 | let diagnostics = self.reload().await; 163 | self.send_diagnostics(diagnostics).await; 164 | } 165 | 166 | pub async fn did_change_watched_files(&mut self, params: DidChangeWatchedFilesParams) { 167 | let DidChangeWatchedFilesParams { changes } = params; 168 | let uris: Vec<_> = changes 169 | .into_iter() 170 | .map(|FileEvent { uri, typ }| { 171 | assert_eq!(typ, FileChangeType::DELETED); 172 | uri 173 | }) 174 | .collect(); 175 | 176 | let mut diagnostics = Diagnostics::new(); 177 | 178 | for uri in uris { 179 | self.client 180 | .log_message( 181 | MessageType::INFO, 182 | format!("Deleting removed document {}", uri), 183 | ) 184 | .await; 185 | 186 | if let Some(removed) = self.remove_document(&uri) { 187 | let (_, empty_diagnostics) = create_empty_diagnostics((&uri, &removed)); 188 | if diagnostics.insert(uri, empty_diagnostics).is_some() { 189 | self.client 190 | .log_message( 191 | MessageType::WARNING, 192 | "\tDuplicate URIs in event payload".to_string(), 193 | ) 194 | .await; 195 | } 196 | } else { 197 | self.client 198 | .log_message( 199 | MessageType::WARNING, 200 | "\tAttempted to delete untracked document".to_string(), 201 | ) 202 | .await; 203 | } 204 | } 205 | 206 | diagnostics.extend(self.reload().await); 207 | self.send_diagnostics(diagnostics).await; 208 | } 209 | 210 | pub async fn did_delete_files(&mut self, params: DeleteFilesParams) { 211 | let DeleteFilesParams { files } = params; 212 | let mut uris = vec![]; 213 | for FileDelete { uri } in files { 214 | match Url::parse(&uri) { 215 | Ok(uri) => uris.push(uri), 216 | Err(e) => { 217 | self.client 218 | .log_message(MessageType::ERROR, format!("Failed to parse URI {}", e)) 219 | .await 220 | } 221 | } 222 | } 223 | 224 | let mut diagnostics = Diagnostics::new(); 225 | 226 | self.client 227 | .log_message(MessageType::INFO, format!("Deleting {} files", uris.len())) 228 | .await; 229 | 230 | for uri in uris { 231 | let removed = self.remove_documents_in_dir(&uri); 232 | if !removed.is_empty() { 233 | for (uri, params) in removed { 234 | self.client 235 | .log_message(MessageType::INFO, format!("\tDeleted {}", uri)) 236 | .await; 237 | 238 | if diagnostics.insert(uri, params).is_some() { 239 | self.client 240 | .log_message( 241 | MessageType::INFO, 242 | "\tDuplicate URIs in event payload".to_string(), 243 | ) 244 | .await; 245 | } 246 | } 247 | } 248 | } 249 | 250 | if !diagnostics.is_empty() { 251 | diagnostics.extend(self.reload().await); 252 | self.send_diagnostics(diagnostics).await; 253 | } 254 | } 255 | 256 | pub fn code_action(&self, params: CodeActionParams) -> Result> { 257 | let CodeActionParams { 258 | context, 259 | range, 260 | text_document, 261 | .. 262 | } = params; 263 | let only = context.only; 264 | let analysis = self.analyses.get(&text_document.uri); 265 | let mut actions = Vec::new(); 266 | 267 | if let Some(analysis) = analysis { 268 | // Inlining 269 | if only 270 | .as_ref() 271 | .map_or(true, |only| only.contains(&CodeActionKind::REFACTOR_INLINE)) 272 | { 273 | let mut rule_name = None; 274 | 275 | for (name, ra) in analysis.rules.iter() { 276 | if let Some(ra) = ra { 277 | if ra.identifier_location.range == range { 278 | rule_name = Some(name); 279 | break; 280 | } 281 | } 282 | } 283 | 284 | if let Some(rule_name) = rule_name { 285 | let ra = self 286 | .get_rule_analysis(&text_document.uri, rule_name) 287 | .expect("should not be called on a builtin with no rule analysis"); 288 | let rule_expression = ra.expression.clone(); 289 | 290 | let mut edits = Vec::new(); 291 | 292 | edits.push(TextEdit { 293 | range: ra.definition_location.range, 294 | new_text: "".to_owned(), 295 | }); 296 | 297 | if let Some(occurrences) = self 298 | .get_rule_analysis(&text_document.uri, rule_name) 299 | .map(|ra| &ra.occurrences) 300 | { 301 | for occurrence in occurrences { 302 | if occurrence.range != ra.identifier_location.range { 303 | edits.push(TextEdit { 304 | range: occurrence.range, 305 | new_text: rule_expression.clone(), 306 | }); 307 | } 308 | } 309 | } 310 | 311 | let mut changes = HashMap::new(); 312 | changes.insert(text_document.uri.clone(), edits); 313 | 314 | actions.push(CodeActionOrCommand::CodeAction(CodeAction { 315 | title: "Inline rule".to_owned(), 316 | kind: Some(CodeActionKind::REFACTOR_INLINE), 317 | edit: Some(WorkspaceEdit { 318 | changes: Some(changes), 319 | document_changes: None, 320 | change_annotations: None, 321 | }), 322 | ..Default::default() 323 | })); 324 | } 325 | } 326 | 327 | if only.as_ref().map_or(true, |only| { 328 | only.contains(&CodeActionKind::REFACTOR_EXTRACT) 329 | }) && range.start.line == range.end.line 330 | { 331 | let document = self.documents.get(&text_document.uri).unwrap(); 332 | let mut lines = document.text.lines(); 333 | let line = lines.nth(range.start.line as usize).unwrap_or(""); 334 | 335 | let mut rule_name_start_idx = 0; 336 | let mut chars = line.graphemes(true); 337 | 338 | while chars.next() == Some(" ") { 339 | rule_name_start_idx += 1; 340 | } 341 | 342 | let name_range = line.get_word_range_at_idx(rule_name_start_idx); 343 | let rule_name = &str_range(line, &name_range); 344 | 345 | if let Some(ra) = self.get_rule_analysis(&text_document.uri, rule_name) { 346 | let mut selected_token = None; 347 | 348 | for (token, location) in ra.tokens.iter() { 349 | if range_contains(&location.range, &range) { 350 | selected_token = Some((token, location)); 351 | break; 352 | } 353 | } 354 | 355 | if let Some((extracted_token, location)) = selected_token { 356 | //TODO: Replace with something more robust, it's horrible 357 | let extracted_token_identifier = 358 | match parser::parse(parser::Rule::node, extracted_token) { 359 | Ok(mut node) => { 360 | let mut next = node.next().unwrap(); 361 | 362 | loop { 363 | match next.as_rule() { 364 | parser::Rule::terminal => { 365 | next = next.into_inner().next().unwrap(); 366 | } 367 | parser::Rule::_push 368 | | parser::Rule::peek_slice 369 | | parser::Rule::identifier 370 | | parser::Rule::string 371 | | parser::Rule::insensitive_string 372 | | parser::Rule::range => { 373 | break Some(next); 374 | } 375 | parser::Rule::opening_paren => { 376 | node = node.next().unwrap().into_inner(); 377 | let next_opt = node 378 | .find(|r| r.as_rule() == parser::Rule::term) 379 | .unwrap() 380 | .into_inner() 381 | .find(|r| r.as_rule() == parser::Rule::node); 382 | 383 | if let Some(new_next) = next_opt { 384 | next = new_next; 385 | } else { 386 | break None; 387 | } 388 | } 389 | _ => unreachable!( 390 | "unexpected rule in node: {:?}", 391 | next.as_rule() 392 | ), 393 | }; 394 | } 395 | .map(|p| p.as_str()) 396 | } 397 | Err(_) => None, 398 | } 399 | .unwrap_or(""); 400 | 401 | if self 402 | .get_rule_analysis(&text_document.uri, extracted_token_identifier) 403 | .is_some() 404 | || BUILTINS.contains(&extracted_token_identifier) 405 | || extracted_token.starts_with('\"') 406 | || extracted_token.starts_with('\'') 407 | || extracted_token.starts_with("PUSH") 408 | || extracted_token.starts_with("PEEK") 409 | || extracted_token.starts_with("^\"") 410 | { 411 | let mut rule_name_number = 0; 412 | let extracted_rule_name = loop { 413 | rule_name_number += 1; 414 | let extracted_rule_name = 415 | format!("{}_{}", rule_name, rule_name_number); 416 | if self 417 | .get_rule_analysis(&text_document.uri, &extracted_rule_name) 418 | .is_none() 419 | { 420 | break extracted_rule_name; 421 | } 422 | }; 423 | 424 | let extracted_rule = format!( 425 | "{} = {{ {} }}", 426 | extracted_rule_name.trim(), 427 | extracted_token.trim(), 428 | ); 429 | 430 | let mut edits = Vec::new(); 431 | 432 | edits.push(TextEdit { 433 | range: Range { 434 | start: Position { 435 | line: location.range.end.line + 1, 436 | character: 0, 437 | }, 438 | end: Position { 439 | line: location.range.end.line + 1, 440 | character: 0, 441 | }, 442 | }, 443 | new_text: format!( 444 | "{}{}\n", 445 | if line.ends_with('\n') { "" } else { "\n" }, 446 | extracted_rule 447 | ), 448 | }); 449 | 450 | let mut changes = HashMap::new(); 451 | changes.insert(text_document.uri.clone(), edits); 452 | 453 | for (url, analysis) in self.analyses.iter() { 454 | for (_, ra) in analysis.rules.iter() { 455 | if let Some(ra) = ra { 456 | for (token, location) in ra.tokens.iter() { 457 | if token == extracted_token { 458 | changes 459 | .entry(url.clone()) 460 | .or_insert_with(Vec::new) 461 | .push(TextEdit { 462 | range: location.range, 463 | new_text: format!( 464 | "{} ", 465 | extracted_rule_name 466 | ), 467 | }); 468 | } 469 | } 470 | } 471 | } 472 | } 473 | 474 | actions.push(CodeActionOrCommand::CodeAction(CodeAction { 475 | title: "Extract into new rule".to_owned(), 476 | kind: Some(CodeActionKind::REFACTOR_EXTRACT), 477 | edit: Some(WorkspaceEdit { 478 | changes: Some(changes), 479 | document_changes: None, 480 | change_annotations: None, 481 | }), 482 | ..Default::default() 483 | })); 484 | } 485 | } 486 | } 487 | } 488 | } 489 | 490 | Ok(Some(actions)) 491 | } 492 | 493 | pub fn completion(&self, params: CompletionParams) -> Result> { 494 | let CompletionParams { 495 | text_document_position, 496 | .. 497 | } = params; 498 | 499 | let document = self 500 | .documents 501 | .get(&text_document_position.text_document.uri) 502 | .unwrap(); 503 | 504 | let mut lines = document.text.lines(); 505 | let line = lines 506 | .nth(text_document_position.position.line as usize) 507 | .unwrap_or(""); 508 | let range = line.get_word_range_at_idx(text_document_position.position.character as usize); 509 | let partial_identifier = &str_range(line, &range); 510 | 511 | if let Some(analysis) = self.analyses.get(&document.uri) { 512 | return Ok(Some(CompletionResponse::Array( 513 | analysis 514 | .rules 515 | .keys() 516 | .filter(|i| partial_identifier.is_empty() || i.starts_with(partial_identifier)) 517 | .map(|i| CompletionItem { 518 | label: i.to_owned(), 519 | kind: Some(CompletionItemKind::FIELD), 520 | ..Default::default() 521 | }) 522 | .collect(), 523 | ))); 524 | } 525 | 526 | Ok(None) 527 | } 528 | 529 | pub fn hover(&self, params: HoverParams) -> Result> { 530 | let HoverParams { 531 | text_document_position_params, 532 | .. 533 | } = params; 534 | let document = self 535 | .documents 536 | .get(&text_document_position_params.text_document.uri) 537 | .unwrap(); 538 | 539 | let mut lines = document.text.lines(); 540 | let line = lines 541 | .nth(text_document_position_params.position.line as usize) 542 | .unwrap_or(""); 543 | let range = 544 | line.get_word_range_at_idx(text_document_position_params.position.character as usize); 545 | let identifier = &str_range(line, &range); 546 | 547 | if let Some(description) = get_builtin_description(identifier) { 548 | return Ok(Some(Hover { 549 | contents: HoverContents::Scalar(MarkedString::String(description.to_owned())), 550 | range: Some(range.into_range(text_document_position_params.position.line)), 551 | })); 552 | } 553 | 554 | if let Some(Some(ra)) = self 555 | .analyses 556 | .get(&document.uri) 557 | .and_then(|a| a.rules.get(identifier)) 558 | { 559 | return Ok(Some(Hover { 560 | contents: HoverContents::Scalar(MarkedString::String(ra.doc.clone())), 561 | range: Some(range.into_range(text_document_position_params.position.line)), 562 | })); 563 | } 564 | 565 | Ok(None) 566 | } 567 | 568 | pub fn rename(&self, params: RenameParams) -> Result> { 569 | let RenameParams { 570 | text_document_position, 571 | new_name, 572 | .. 573 | } = params; 574 | 575 | let document = self 576 | .documents 577 | .get(&text_document_position.text_document.uri) 578 | .unwrap(); 579 | let line = document 580 | .text 581 | .lines() 582 | .nth(text_document_position.position.line as usize) 583 | .unwrap_or(""); 584 | let old_identifier = &str_range( 585 | line, 586 | &line.get_word_range_at_idx(text_document_position.position.character as usize), 587 | ); 588 | let mut edits = Vec::new(); 589 | 590 | if let Some(occurrences) = self 591 | .get_rule_analysis(&document.uri, old_identifier) 592 | .map(|ra| &ra.occurrences) 593 | { 594 | for location in occurrences { 595 | edits.push(TextEdit { 596 | range: location.range, 597 | new_text: new_name.clone(), 598 | }); 599 | } 600 | } 601 | 602 | Ok(Some(WorkspaceEdit { 603 | change_annotations: None, 604 | changes: None, 605 | document_changes: Some(DocumentChanges::Edits(vec![TextDocumentEdit { 606 | text_document: OptionalVersionedTextDocumentIdentifier { 607 | uri: text_document_position.text_document.uri, 608 | version: Some(document.version), 609 | }, 610 | edits: edits.into_iter().map(OneOf::Left).collect(), 611 | }])), 612 | })) 613 | } 614 | 615 | pub fn goto_declaration( 616 | &self, 617 | params: GotoDeclarationParams, 618 | ) -> Result> { 619 | let GotoDeclarationParams { 620 | text_document_position_params, 621 | .. 622 | } = params; 623 | 624 | let document = self 625 | .documents 626 | .get(&text_document_position_params.text_document.uri) 627 | .unwrap(); 628 | 629 | let mut lines = document.text.lines(); 630 | let line = lines 631 | .nth(text_document_position_params.position.line as usize) 632 | .unwrap_or(""); 633 | 634 | let range = 635 | line.get_word_range_at_idx(text_document_position_params.position.character as usize); 636 | let identifier = &str_range(line, &range); 637 | 638 | if let Some(location) = self 639 | .get_rule_analysis(&document.uri, identifier) 640 | .map(|ra| &ra.definition_location) 641 | { 642 | return Ok(Some(GotoDeclarationResponse::Scalar(Location { 643 | uri: text_document_position_params.text_document.uri, 644 | range: location.range, 645 | }))); 646 | } 647 | 648 | Ok(None) 649 | } 650 | 651 | pub fn goto_definition( 652 | &self, 653 | params: GotoDefinitionParams, 654 | ) -> Result> { 655 | let GotoDefinitionParams { 656 | text_document_position_params, 657 | .. 658 | } = params; 659 | 660 | let document = self 661 | .documents 662 | .get(&text_document_position_params.text_document.uri) 663 | .unwrap(); 664 | 665 | let mut lines = document.text.lines(); 666 | let line = lines 667 | .nth(text_document_position_params.position.line as usize) 668 | .unwrap_or(""); 669 | let range = 670 | line.get_word_range_at_idx(text_document_position_params.position.character as usize); 671 | let identifier = &str_range(line, &range); 672 | 673 | if let Some(location) = self 674 | .get_rule_analysis(&document.uri, identifier) 675 | .map(|ra| &ra.definition_location) 676 | { 677 | return Ok(Some(GotoDeclarationResponse::Scalar(Location { 678 | uri: text_document_position_params.text_document.uri, 679 | range: location.range, 680 | }))); 681 | } 682 | 683 | Ok(None) 684 | } 685 | 686 | pub fn references(&self, params: ReferenceParams) -> Result>> { 687 | let ReferenceParams { 688 | text_document_position, 689 | .. 690 | } = params; 691 | 692 | let document = self 693 | .documents 694 | .get(&text_document_position.text_document.uri) 695 | .unwrap(); 696 | 697 | let mut lines = document.text.lines(); 698 | let line = lines 699 | .nth(text_document_position.position.line as usize) 700 | .unwrap_or(""); 701 | let range = line.get_word_range_at_idx(text_document_position.position.character as usize); 702 | let identifier = &str_range(line, &range); 703 | 704 | Ok(self 705 | .get_rule_analysis(&document.uri, identifier) 706 | .map(|ra| ra.occurrences.clone())) 707 | } 708 | 709 | pub fn formatting(&self, params: DocumentFormattingParams) -> Result>> { 710 | let DocumentFormattingParams { text_document, .. } = params; 711 | 712 | let document = self.documents.get(&text_document.uri).unwrap(); 713 | let input = document.text.as_str(); 714 | 715 | let fmt = pest_fmt::Formatter::new(input); 716 | if let Ok(formatted) = fmt.format() { 717 | let lines = document.text.lines(); 718 | let last_line = lines.clone().last().unwrap_or(""); 719 | let range = Range::new( 720 | Position::new(0, 0), 721 | Position::new(lines.count() as u32, last_line.len() as u32), 722 | ); 723 | return Ok(Some(vec![TextEdit::new(range, formatted)])); 724 | } 725 | 726 | Ok(None) 727 | } 728 | } 729 | 730 | impl PestLanguageServerImpl { 731 | async fn reload(&mut self) -> Diagnostics { 732 | self.client 733 | .log_message(MessageType::INFO, "Reloading all diagnostics".to_string()) 734 | .await; 735 | let mut diagnostics = Diagnostics::new(); 736 | 737 | for (url, document) in &self.documents { 738 | self.client 739 | .log_message( 740 | MessageType::INFO, 741 | format!("\tReloading diagnostics for {}", url), 742 | ) 743 | .await; 744 | 745 | let pairs = match parser::parse(parser::Rule::grammar_rules, document.text.as_str()) { 746 | Ok(pairs) => Ok(pairs), 747 | Err(error) => Err(vec![error]), 748 | }; 749 | 750 | if let Ok(pairs) = pairs { 751 | if let Err(errors) = validate_pairs(pairs.clone()) { 752 | diagnostics.insert( 753 | url.clone(), 754 | PublishDiagnosticsParams::new( 755 | url.clone(), 756 | errors.into_diagnostics(), 757 | Some(document.version), 758 | ), 759 | ); 760 | } else { 761 | let (_, empty_diagnostics) = create_empty_diagnostics((url, document)); 762 | diagnostics.insert(url.clone(), empty_diagnostics); 763 | } 764 | 765 | self.analyses 766 | .entry(url.clone()) 767 | .or_insert_with(|| Analysis { 768 | doc_url: url.clone(), 769 | rules: HashMap::new(), 770 | }) 771 | .update_from(pairs); 772 | } else if let Err(errors) = pairs { 773 | diagnostics.insert( 774 | url.clone(), 775 | PublishDiagnosticsParams::new( 776 | url.clone(), 777 | errors.into_diagnostics(), 778 | Some(document.version), 779 | ), 780 | ); 781 | } 782 | 783 | if let Some(analysis) = self.analyses.get(url) { 784 | let mut unused_diagnostics = Vec::new(); 785 | for (rule_name, rule_location) in 786 | analysis.get_unused_rules().iter().filter(|(rule_name, _)| { 787 | !self.config.always_used_rule_names.contains(rule_name) 788 | }) 789 | { 790 | unused_diagnostics.push(Diagnostic::new( 791 | rule_location.range, 792 | Some(DiagnosticSeverity::WARNING), 793 | None, 794 | Some("Pest Language Server".to_owned()), 795 | format!("Rule {} is unused", rule_name), 796 | None, 797 | None, 798 | )); 799 | } 800 | 801 | if unused_diagnostics.len() > 1 { 802 | diagnostics 803 | .entry(url.to_owned()) 804 | .or_insert_with(|| create_empty_diagnostics((url, document)).1) 805 | .diagnostics 806 | .extend(unused_diagnostics); 807 | } 808 | } 809 | } 810 | 811 | diagnostics 812 | } 813 | 814 | fn upsert_document(&mut self, doc: TextDocumentItem) -> Option { 815 | self.documents.insert(doc.uri.clone(), doc) 816 | } 817 | 818 | fn remove_document(&mut self, uri: &Url) -> Option { 819 | self.documents.remove(uri) 820 | } 821 | 822 | fn remove_documents_in_dir(&mut self, dir: &Url) -> Diagnostics { 823 | let (in_dir, not_in_dir): (Documents, Documents) = 824 | self.documents.clone().into_iter().partition(|(uri, _)| { 825 | let maybe_segments = dir.path_segments().zip(uri.path_segments()); 826 | let compare_paths = |(l, r): (Split<_>, Split<_>)| l.zip(r).all(|(l, r)| l == r); 827 | maybe_segments.map_or(false, compare_paths) 828 | }); 829 | 830 | self.documents = not_in_dir; 831 | in_dir.iter().map(create_empty_diagnostics).collect() 832 | } 833 | 834 | async fn send_diagnostics(&self, diagnostics: Diagnostics) { 835 | for PublishDiagnosticsParams { 836 | uri, 837 | diagnostics, 838 | version, 839 | } in diagnostics.into_values() 840 | { 841 | self.client 842 | .publish_diagnostics(uri.clone(), diagnostics, version) 843 | .await; 844 | } 845 | } 846 | 847 | fn get_rule_analysis(&self, uri: &Url, rule_name: &str) -> Option<&RuleAnalysis> { 848 | self.analyses 849 | .get(uri) 850 | .and_then(|analysis| analysis.rules.get(rule_name)) 851 | .and_then(|ra| ra.as_ref()) 852 | } 853 | } 854 | -------------------------------------------------------------------------------- /language-server/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::env::args; 3 | use std::process::exit; 4 | use std::sync::Arc; 5 | 6 | use capabilities::capabilities; 7 | use config::Config; 8 | use lsp::PestLanguageServerImpl; 9 | use tokio::sync::RwLock; 10 | use tower_lsp::jsonrpc::Result; 11 | use tower_lsp::lsp_types::{ 12 | CodeActionParams, CodeActionResponse, DidChangeConfigurationParams, 13 | DidChangeWatchedFilesParams, DocumentFormattingParams, InitializeParams, InitializeResult, 14 | InitializedParams, 15 | }; 16 | use tower_lsp::{ 17 | lsp_types::{ 18 | request::{GotoDeclarationParams, GotoDeclarationResponse}, 19 | CompletionParams, CompletionResponse, DeleteFilesParams, DidChangeTextDocumentParams, 20 | DidOpenTextDocumentParams, GotoDefinitionParams, GotoDefinitionResponse, Hover, 21 | HoverParams, Location, ReferenceParams, RenameParams, TextEdit, WorkspaceEdit, 22 | }, 23 | LanguageServer, 24 | }; 25 | use tower_lsp::{Client, LspService, Server}; 26 | 27 | mod analysis; 28 | mod builtins; 29 | mod capabilities; 30 | mod config; 31 | mod helpers; 32 | mod lsp; 33 | mod update_checker; 34 | 35 | #[derive(Debug)] 36 | /// The async-ready language server. You probably want [PestLanguageServerImpl] instead. 37 | pub struct PestLanguageServer(Arc>); 38 | 39 | impl PestLanguageServer { 40 | pub fn new(client: Client) -> Self { 41 | Self(Arc::new(RwLock::new(PestLanguageServerImpl { 42 | analyses: HashMap::new(), 43 | client, 44 | config: Config::default(), 45 | documents: HashMap::new(), 46 | }))) 47 | } 48 | } 49 | 50 | #[tower_lsp::async_trait] 51 | impl LanguageServer for PestLanguageServer { 52 | async fn initialize(&self, _: InitializeParams) -> Result { 53 | Ok(capabilities()) 54 | } 55 | 56 | async fn initialized(&self, params: InitializedParams) { 57 | self.0.write().await.initialized(params).await; 58 | } 59 | 60 | async fn shutdown(&self) -> Result<()> { 61 | self.0.read().await.shutdown().await 62 | } 63 | 64 | async fn did_change_configuration(&self, params: DidChangeConfigurationParams) { 65 | self.0.write().await.did_change_configuration(params).await; 66 | } 67 | 68 | async fn did_open(&self, params: DidOpenTextDocumentParams) { 69 | self.0.write().await.did_open(params).await; 70 | } 71 | 72 | async fn did_change(&self, params: DidChangeTextDocumentParams) { 73 | self.0.write().await.did_change(params).await; 74 | } 75 | 76 | async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) { 77 | self.0.write().await.did_change_watched_files(params).await; 78 | } 79 | 80 | async fn did_delete_files(&self, params: DeleteFilesParams) { 81 | self.0.write().await.did_delete_files(params).await; 82 | } 83 | 84 | async fn code_action(&self, params: CodeActionParams) -> Result> { 85 | self.0.read().await.code_action(params) 86 | } 87 | 88 | async fn completion(&self, params: CompletionParams) -> Result> { 89 | self.0.read().await.completion(params) 90 | } 91 | 92 | async fn hover(&self, params: HoverParams) -> Result> { 93 | self.0.read().await.hover(params) 94 | } 95 | 96 | async fn rename(&self, params: RenameParams) -> Result> { 97 | self.0.read().await.rename(params) 98 | } 99 | 100 | async fn goto_declaration( 101 | &self, 102 | params: GotoDeclarationParams, 103 | ) -> Result> { 104 | self.0.read().await.goto_declaration(params) 105 | } 106 | 107 | async fn goto_definition( 108 | &self, 109 | params: GotoDefinitionParams, 110 | ) -> Result> { 111 | self.0.read().await.goto_definition(params) 112 | } 113 | 114 | async fn references(&self, params: ReferenceParams) -> Result>> { 115 | self.0.read().await.references(params) 116 | } 117 | 118 | async fn formatting(&self, params: DocumentFormattingParams) -> Result>> { 119 | self.0.read().await.formatting(params) 120 | } 121 | } 122 | 123 | #[tokio::main] 124 | async fn main() { 125 | for arg in args().skip(1) { 126 | match arg.as_str() { 127 | "--version" => { 128 | println!("{}", env!("CARGO_PKG_VERSION")); 129 | exit(0); 130 | } 131 | _ => eprintln!("Unexpected argument: {}", arg), 132 | } 133 | } 134 | 135 | let stdin = tokio::io::stdin(); 136 | let stdout = tokio::io::stdout(); 137 | 138 | let (service, socket) = LspService::new(PestLanguageServer::new); 139 | Server::new(stdin, stdout, socket).serve(service).await; 140 | } 141 | -------------------------------------------------------------------------------- /language-server/src/update_checker.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use reqwest::ClientBuilder; 4 | 5 | /// Checks crates.io for updates to pest-language-server. 6 | /// Returns the latest version if not already installed, otherwise [None]. 7 | pub async fn check_for_updates() -> Option { 8 | let client = ClientBuilder::new() 9 | .user_agent(concat!( 10 | env!("CARGO_PKG_NAME"), 11 | "/", 12 | env!("CARGO_PKG_VERSION") 13 | )) 14 | .timeout(Duration::from_secs(2)) 15 | .build(); 16 | 17 | if let Ok(client) = client { 18 | let response = client 19 | .get("https://crates.io/api/v1/crates/pest-language-server") 20 | .send() 21 | .await; 22 | 23 | if let Ok(response) = response { 24 | return response 25 | .json::() 26 | .await 27 | .map_or(None, |json| { 28 | let version = json["crate"]["max_version"].as_str()?; 29 | 30 | if version != env!("CARGO_PKG_VERSION") { 31 | Some(version.to_string()) 32 | } else { 33 | None 34 | } 35 | }); 36 | } 37 | } 38 | 39 | None 40 | } 41 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pest-ide-support", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": {} 6 | } 7 | -------------------------------------------------------------------------------- /pest.tmLanguage: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | $schema 6 | https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json 7 | name 8 | Pest 9 | patterns 10 | 11 | 12 | include 13 | #keywords 14 | 15 | 16 | include 17 | #strings 18 | 19 | 20 | include 21 | #tags 22 | 23 | 24 | include 25 | #comments 26 | 27 | 28 | include 29 | #multilinecomments 30 | 31 | 32 | repository 33 | 34 | keywords 35 | 36 | patterns 37 | 38 | 39 | name 40 | keyword.control.pest 41 | match 42 | \b(PUSH|POP|PEEK|POP_ALL|PEEK_ALL)\b 43 | 44 | 45 | name 46 | variable.language.implicit.pest 47 | match 48 | \b(WHITESPACE|COMMENT)\b 49 | 50 | 51 | name 52 | constant.language.borders.pest 53 | match 54 | \b(SOI|EOI)\b 55 | 56 | 57 | name 58 | constant.language.character.ascii.pest 59 | match 60 | \b(NEWLINE|ASCII_DIGIT|ASCII_NONZERO_DIGIT|ASCII_BIN_DIGIT|ASCII_OCT_DIGIT|ASCII_HEX_DIGIT|ASCII_ALPHA_LOWER|ASCII_ALPHA_UPPER|ASCII_ALPHA|ASCII_ALPHANUMERIC)\b 61 | 62 | 63 | name 64 | constant.language.character.unicode.letter.pest 65 | match 66 | \b(ANY|LETTER|CASED_LETTER|UPPERCASE_LETTER|LOWERCASE_LETTER|TITLECASE_LETTER|MODIFIER_LETTER|OTHER_LETTER)\b 67 | 68 | 69 | name 70 | constant.language.character.unicode.mark.pest 71 | match 72 | \b(MARK|COMBINING_SPACING_MARK|ENCLOSING_MARK|NONSPACING_MARK)\b 73 | 74 | 75 | name 76 | constant.language.character.unicode.number.pest 77 | match 78 | \b(NUMBER|DECIMAL_NUMBER|LETTER_NUMBER|OTHER_NUMBER)\b 79 | 80 | 81 | name 82 | constant.language.character.unicode.punctuation.pest 83 | match 84 | \b(PUNCTUATION|CONNECTOR_PUNCTUATION|DASH_PUNCTUATION|OPEN_PUNCTUATION|CLOSE_PUNCTUATION|INITIAL_PUNCTUATION|FINAL_PUNCTUATION|OTHER_PUNCTUATION)\b 85 | 86 | 87 | name 88 | constant.language.character.unicode.symbol.pest 89 | match 90 | \b(SYMBOL|MATH_SYMBOL|CURRENCY_SYMBOL|MODIFIER_SYMBOL|OTHER_SYMBOL)\b 91 | 92 | 93 | name 94 | constant.language.character.unicode.separator.pest 95 | match 96 | \b(SEPARATOR|SPACE_SEPARATOR|LINE_SEPARATOR|PARAGRAPH_SEPARATOR)\b 97 | 98 | 99 | name 100 | constant.language.character.unicode.other.pest 101 | match 102 | \b(CONTROL|FORMAT|SURROGATE|PRIVATE_USE|UNASSIGNED)\b 103 | 104 | 105 | name 106 | constant.language.character.unicode.binary.pest 107 | match 108 | \b(ALPHABETIC|BIDI_CONTROL|CASE_IGNORABLE|CASED|CHANGES_WHEN_CASEFOLDED|CHANGES_WHEN_CASEMAPPED|CHANGES_WHEN_LOWERCASED|CHANGES_WHEN_TITLECASED|CHANGES_WHEN_UPPERCASED|DASH|DEFAULT_IGNORABLE_CODE_POINT|DEPRECATED|DIACRITIC|EXTENDER|GRAPHEME_BASE|GRAPHEME_EXTEND|GRAPHEME_LINK|HEX_DIGIT|HYPHEN|IDS_BINARY_OPERATOR|IDS_TRINARY_OPERATOR|ID_CONTINUE|ID_START|IDEOGRAPHIC|JOIN_CONTROL|LOGICAL_ORDER_EXCEPTION|LOWERCASE|MATH|NONCHARACTER_CODE_POINT|OTHER_ALPHABETIC|OTHER_DEFAULT_IGNORABLE_CODE_POINT|OTHER_GRAPHEME_EXTEND|OTHER_ID_CONTINUE|OTHER_ID_START|OTHER_LOWERCASE|OTHER_MATH|OTHER_UPPERCASE|PATTERN_SYNTAX|PATTERN_WHITE_SPACE|PREPENDED_CONCATENATION_MARK|QUOTATION_MARK|RADICAL|REGIONAL_INDICATOR|SENTENCE_TERMINAL|SOFT_DOTTED|TERMINAL_PUNCTUATION|UNIFIED_IDEOGRAPH|UPPERCASE|VARIATION_SELECTOR|WHITE_SPACE|XID_CONTINUE|XID_START)\b 109 | 110 | 111 | name 112 | keyword.operator.pest 113 | match 114 | -|\.\.|!|~|_|@|\$ 115 | 116 | 117 | name 118 | keyword.operator.repetition.pest 119 | match 120 | \+|\*|\?|{\s*[0-9]+\s*,\s*}|{\s*,\s*[0-9]+\s*}|{\s*[0-9]+\s*,\s*[0-9]+\s*} 121 | 122 | 123 | name 124 | variable.name.pest 125 | match 126 | [A-Za-z_][A-Za-z0-9_]* 127 | 128 | 129 | 130 | strings 131 | 132 | patterns 133 | 134 | 135 | name 136 | string.quoted.double.pest 137 | begin 138 | " 139 | end 140 | " 141 | patterns 142 | 143 | 144 | include 145 | #characterescape 146 | 147 | 148 | 149 | 150 | name 151 | string.quoted.single.pest 152 | begin 153 | ' 154 | end 155 | ' 156 | patterns 157 | 158 | 159 | include 160 | #characterescape 161 | 162 | 163 | 164 | 165 | 166 | tags 167 | 168 | patterns 169 | 170 | 171 | name 172 | entity.name.tag.pest 173 | match 174 | #[A-Za-z_][A-Za-z0-9_]* 175 | 176 | 177 | name 178 | keyword.operator.assignment.pest 179 | match 180 | = 181 | 182 | 183 | 184 | comments 185 | 186 | patterns 187 | 188 | 189 | name 190 | comment.line.pest 191 | match 192 | //.* 193 | 194 | 195 | 196 | multilinecomments 197 | 198 | patterns 199 | 200 | 201 | name 202 | comment.block.pest 203 | begin 204 | /\* 205 | end 206 | \*/ 207 | patterns 208 | 209 | 210 | include 211 | #multilinecomments 212 | 213 | 214 | 215 | 216 | 217 | characterescape 218 | 219 | name 220 | constant.character.escape.pest 221 | match 222 | ((\\u\{[a-fA-F0-9]{4}\})|(\\.)) 223 | 224 | 225 | scopeName 226 | source.pest 227 | 228 | 229 | -------------------------------------------------------------------------------- /sublime-text/pest.tmLanguage: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | $schema 6 | https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json 7 | name 8 | Pest 9 | patterns 10 | 11 | 12 | include 13 | #keywords 14 | 15 | 16 | include 17 | #strings 18 | 19 | 20 | include 21 | #tags 22 | 23 | 24 | include 25 | #comments 26 | 27 | 28 | include 29 | #multilinecomments 30 | 31 | 32 | repository 33 | 34 | keywords 35 | 36 | patterns 37 | 38 | 39 | name 40 | keyword.control.pest 41 | match 42 | \b(PUSH|POP|PEEK|POP_ALL|PEEK_ALL)\b 43 | 44 | 45 | name 46 | variable.language.implicit.pest 47 | match 48 | \b(WHITESPACE|COMMENT)\b 49 | 50 | 51 | name 52 | constant.language.borders.pest 53 | match 54 | \b(SOI|EOI)\b 55 | 56 | 57 | name 58 | constant.language.character.ascii.pest 59 | match 60 | \b(NEWLINE|ASCII_DIGIT|ASCII_NONZERO_DIGIT|ASCII_BIN_DIGIT|ASCII_OCT_DIGIT|ASCII_HEX_DIGIT|ASCII_ALPHA_LOWER|ASCII_ALPHA_UPPER|ASCII_ALPHA|ASCII_ALPHANUMERIC)\b 61 | 62 | 63 | name 64 | constant.language.character.unicode.letter.pest 65 | match 66 | \b(ANY|LETTER|CASED_LETTER|UPPERCASE_LETTER|LOWERCASE_LETTER|TITLECASE_LETTER|MODIFIER_LETTER|OTHER_LETTER)\b 67 | 68 | 69 | name 70 | constant.language.character.unicode.mark.pest 71 | match 72 | \b(MARK|COMBINING_SPACING_MARK|ENCLOSING_MARK|NONSPACING_MARK)\b 73 | 74 | 75 | name 76 | constant.language.character.unicode.number.pest 77 | match 78 | \b(NUMBER|DECIMAL_NUMBER|LETTER_NUMBER|OTHER_NUMBER)\b 79 | 80 | 81 | name 82 | constant.language.character.unicode.punctuation.pest 83 | match 84 | \b(PUNCTUATION|CONNECTOR_PUNCTUATION|DASH_PUNCTUATION|OPEN_PUNCTUATION|CLOSE_PUNCTUATION|INITIAL_PUNCTUATION|FINAL_PUNCTUATION|OTHER_PUNCTUATION)\b 85 | 86 | 87 | name 88 | constant.language.character.unicode.symbol.pest 89 | match 90 | \b(SYMBOL|MATH_SYMBOL|CURRENCY_SYMBOL|MODIFIER_SYMBOL|OTHER_SYMBOL)\b 91 | 92 | 93 | name 94 | constant.language.character.unicode.separator.pest 95 | match 96 | \b(SEPARATOR|SPACE_SEPARATOR|LINE_SEPARATOR|PARAGRAPH_SEPARATOR)\b 97 | 98 | 99 | name 100 | constant.language.character.unicode.other.pest 101 | match 102 | \b(CONTROL|FORMAT|SURROGATE|PRIVATE_USE|UNASSIGNED)\b 103 | 104 | 105 | name 106 | constant.language.character.unicode.binary.pest 107 | match 108 | \b(ALPHABETIC|BIDI_CONTROL|CASE_IGNORABLE|CASED|CHANGES_WHEN_CASEFOLDED|CHANGES_WHEN_CASEMAPPED|CHANGES_WHEN_LOWERCASED|CHANGES_WHEN_TITLECASED|CHANGES_WHEN_UPPERCASED|DASH|DEFAULT_IGNORABLE_CODE_POINT|DEPRECATED|DIACRITIC|EXTENDER|GRAPHEME_BASE|GRAPHEME_EXTEND|GRAPHEME_LINK|HEX_DIGIT|HYPHEN|IDS_BINARY_OPERATOR|IDS_TRINARY_OPERATOR|ID_CONTINUE|ID_START|IDEOGRAPHIC|JOIN_CONTROL|LOGICAL_ORDER_EXCEPTION|LOWERCASE|MATH|NONCHARACTER_CODE_POINT|OTHER_ALPHABETIC|OTHER_DEFAULT_IGNORABLE_CODE_POINT|OTHER_GRAPHEME_EXTEND|OTHER_ID_CONTINUE|OTHER_ID_START|OTHER_LOWERCASE|OTHER_MATH|OTHER_UPPERCASE|PATTERN_SYNTAX|PATTERN_WHITE_SPACE|PREPENDED_CONCATENATION_MARK|QUOTATION_MARK|RADICAL|REGIONAL_INDICATOR|SENTENCE_TERMINAL|SOFT_DOTTED|TERMINAL_PUNCTUATION|UNIFIED_IDEOGRAPH|UPPERCASE|VARIATION_SELECTOR|WHITE_SPACE|XID_CONTINUE|XID_START)\b 109 | 110 | 111 | name 112 | keyword.operator.pest 113 | match 114 | -|\.\.|!|~|_|@|\$ 115 | 116 | 117 | name 118 | keyword.operator.repetition.pest 119 | match 120 | \+|\*|\?|{\s*[0-9]+\s*,\s*}|{\s*,\s*[0-9]+\s*}|{\s*[0-9]+\s*,\s*[0-9]+\s*} 121 | 122 | 123 | name 124 | variable.name.pest 125 | match 126 | [A-Za-z_][A-Za-z0-9_]* 127 | 128 | 129 | 130 | strings 131 | 132 | patterns 133 | 134 | 135 | name 136 | string.quoted.double.pest 137 | begin 138 | " 139 | end 140 | " 141 | patterns 142 | 143 | 144 | include 145 | #characterescape 146 | 147 | 148 | 149 | 150 | name 151 | string.quoted.single.pest 152 | begin 153 | ' 154 | end 155 | ' 156 | patterns 157 | 158 | 159 | include 160 | #characterescape 161 | 162 | 163 | 164 | 165 | 166 | tags 167 | 168 | patterns 169 | 170 | 171 | name 172 | entity.name.tag.pest 173 | match 174 | #[A-Za-z_][A-Za-z0-9_]* 175 | 176 | 177 | name 178 | keyword.operator.assignment.pest 179 | match 180 | = 181 | 182 | 183 | 184 | comments 185 | 186 | patterns 187 | 188 | 189 | name 190 | comment.line.pest 191 | match 192 | //.* 193 | 194 | 195 | 196 | multilinecomments 197 | 198 | patterns 199 | 200 | 201 | name 202 | comment.block.pest 203 | begin 204 | /\* 205 | end 206 | \*/ 207 | patterns 208 | 209 | 210 | include 211 | #multilinecomments 212 | 213 | 214 | 215 | 216 | 217 | characterescape 218 | 219 | name 220 | constant.character.escape.pest 221 | match 222 | ((\\u\{[a-fA-F0-9]{4}\})|(\\.)) 223 | 224 | 225 | scopeName 226 | source.pest 227 | 228 | 229 | -------------------------------------------------------------------------------- /vscode/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: ["./node_modules/gts/"], 4 | ignorePatterns: ["node_modules", "build"], 5 | rules: { 6 | quotes: ["error", "double"], 7 | indent: ["off"], 8 | "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "_" }], 9 | }, 10 | overrides: [ 11 | { 12 | files: ["**/*.ts"], 13 | extends: [ 14 | "plugin:@typescript-eslint/recommended", 15 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 16 | ], 17 | parserOptions: { 18 | tsconfigRootDir: __dirname, 19 | project: ["./client/tsconfig.json", "./test/tsconfig.json"], 20 | }, 21 | }, 22 | ], 23 | }; 24 | -------------------------------------------------------------------------------- /vscode/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | *.vsix 3 | -------------------------------------------------------------------------------- /vscode/.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build -------------------------------------------------------------------------------- /vscode/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require("gts/.prettierrc.json"), 3 | bracketSpacing: true, 4 | singleQuote: false, 5 | useTabs: true, 6 | }; 7 | -------------------------------------------------------------------------------- /vscode/.vscodeignore: -------------------------------------------------------------------------------- 1 | # Ignore everything. 2 | ** 3 | 4 | # Selectively un-ignore files we want included in the released extension. 5 | !README.md 6 | !LICENSE 7 | !package.json 8 | !language-configuration.json 9 | !build/client.js 10 | !build/server.js 11 | !syntaxes/pest.tmLanguage.json 12 | !icon.png -------------------------------------------------------------------------------- /vscode/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes will be documented in this file. 4 | 5 | 6 | 7 | ## v0.3.11 8 | 9 | - fix(ci): update Node version for OpenVSX publishing. 10 | 11 | ## v0.3.10 12 | 13 | - [[sbillig](https://github.com/sbillig)] feat(analysis): don't emit unused rule warning if the rule name begins with an underscore (#88) 14 | 15 | ## v0.3.9 16 | 17 | - fix(lsp): use rustls rather than native-tls for reqwest 18 | 19 | ## v0.3.8 20 | 21 | - feat(ci): publish binaries to releases, fixes #51 22 | - chore(lsp): update dependencies 23 | 24 | ## v0.3.7 25 | 26 | - feat(deps): update all deps, fixes #50 27 | 28 | ## v0.3.6 29 | 30 | - [[Fluffyalien1422](https://github.com/Fluffyalien1422)] fix(vscode): add quotes around path when invoking the LS (#41) 31 | 32 | ## v0.3.5 33 | 34 | - fix(ci): fix the publish workflow 35 | 36 | ## v0.3.4 37 | 38 | - [[notpeter](https://github.com/notpeter)] fix(server): off-by-one error (#37) 39 | - [[notpeter](https://github.com/notpeter)] fix(grammar): issues with highlighting rules that started with a built-in's name (#38) 40 | 41 | ## v0.3.3 42 | 43 | - fix(server): hotfix for #28, ranges (along with other tokens like insensitive strings) no longer crash the server 44 | 45 | A proper fix for the code causing the crash in #28 will be released, but I do not have the time at the moment. 46 | 47 | ## v0.3.2 48 | 49 | - fix(vscode): update checker is now enabled by default, and some of its logic 50 | has been modified and fixed. It also supports cancellation of the install task 51 | - fix(vscode): give defaults to all config options 52 | - fix(server): fix crash with code actions 53 | 54 | ## v0.3.1 55 | 56 | - revert(server): revert performance marks temporarily, while they are 57 | refactored into a more generic crate 58 | 59 | ## v0.3.0 60 | 61 | - feat(server): add performance marks for debugging 62 | - feat(server): simple rule extraction support 63 | - fix(server): validate AST to catch errors like non-progressing expressions 64 | 65 | ## v0.2.2 66 | 67 | - feat(vscode): allow relative paths in `pestIdeTools.serverPath` 68 | - fix(vscode): allow `pestIdeTools.serverPath` to be `null` in the schema 69 | - fix(server): CJK/non-ascii characters no longer crash the server 70 | - fix(server): add a CJK test case to the manual testing recommendations 71 | 72 | ## v0.2.1 73 | 74 | - fix(vscode): scan both stdout and stderr of Cargo commands, fixes some issues 75 | with installation flow 76 | - feat(*): documentation, issue templates 77 | - feat(sublime): begin publishing a sublime text package 78 | - fix(server, vscode): server now hot-reloads config updates more reliably 79 | - fix(server, vscode): bump problematic dependencies (love the JS ecosystem...a 80 | CVE a day keeps the doctor away) 81 | - feat(server): add rule inlining code action 82 | - feat(server): ignore unused rule name analysis if there is only one unused 83 | rule (hack fix) 84 | 85 | ## v0.2.0 86 | 87 | - feat(*): port to tower lsp 88 | - This will allow the usage of this LS by other IDEs. 89 | - The vscode extension will prompt you to download the server. 90 | - Other IDEs will have to have the LS installed via `cargo install`. 91 | - feat(*): add configuration options 92 | - feat(server, #6): diagnostic for unused rules 93 | - feat(server, #7): show rule docs (`///`) on hover 94 | - fix(server, #8): solve issue relating to 0 vs 1 indexing causing diagnostics 95 | to occur at the wrong locations 96 | - feat(server): add a version checker 97 | - feat(readme, #2): update readme and add demo gif 98 | - feat(ci, #4): automatically populate changelog 99 | - fix(ci): lint all rust code 100 | 101 | ## v0.1.2 102 | 103 | - feat: upgrade pest v2.5.6, pest-fmt v0.2.3. See 104 | [Release Notes](https://github.com/pest-parser/pest/releases/tag/v2.5.6). 105 | - fix(server): solve issue relating to 0 vs 1 indexing. 106 | - feat(server): suggest user-defined rule names in intellisense. 107 | 108 | ## v0.1.1 109 | 110 | - feat(server): add hover information for `SOI` and `EOI` 111 | - fix(ci): allow the release workflow to create releases. 112 | - fix(vscode): add a readme for the vscode extension. 113 | - fix(vscode): add a changelog. 114 | 115 | ## v0.1.0 116 | 117 | - Initial release 118 | -------------------------------------------------------------------------------- /vscode/LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /vscode/README.md: -------------------------------------------------------------------------------- 1 | # Pest IDE Tools 2 | 3 | _IDE support for [Pest](https://pest.rs), via the LSP._ 4 | 5 | This repository contains an implementation of the _Language Server Protocol_ in Rust, for 6 | the Pest parser generator. 7 | 8 |

9 | A demo of the Pest VSCode extension. 10 |

11 | 12 | ## Features 13 | 14 | - Error reporting. 15 | - Warnings for unused rules. 16 | - Syntax highlighting definitions available. 17 | - Rename rules. 18 | - Go to rule declaration, definition, or references. 19 | - Hover information for built-in rules and documented rules. 20 | - Autocompletion of defined rule names. 21 | - Formatting. 22 | - Update checking. 23 | 24 | Please see the 25 | [issues page](https://github.com/pest-parser/pest-ide-tools/issues) to suggest 26 | features or view previous suggestions. 27 | 28 | ## Usage 29 | 30 | ### Supported IDEs 31 | 32 | - [Visual Studio Code](https://marketplace.visualstudio.com/items?itemName=pest.pest-ide-tools) 33 | - VSCode has a pre-built extension that can compile, update, and start up the language server. It also includes syntax highlighting definitions. 34 | 35 | Due to the usage of the LSP by this project, adding support for new IDEs should 36 | be far more achievable than a custom implementation for each editor. Please see the [tracking issue](https://github.com/pest-parser/pest-ide-tools/issues/10) to request support for another IDE or view the current status of IDE support. 37 | 38 | ### Config 39 | 40 | The method of updating your config is editor specific. 41 | 42 | The available options are: 43 | 44 | ```jsonc 45 | { 46 | // Set a custom path to a Pest LS binary. 47 | "pestIdeTools.serverPath": "/path/to/binary", 48 | // Check for updates to the Pest LS binary via crates.io 49 | "pestIdeTools.checkForUpdates": true, 50 | // Ignore specific rule names for the unused rules diagnostics (useful for specifying root rules) 51 | "pestIdeTools.alwaysUsedRuleNames": [ 52 | "rule_one", 53 | "rule_two" 54 | ] 55 | } 56 | ``` 57 | 58 | ## Development 59 | 60 | This repository uses a [Taskfile](https://taskfile.dev); install 61 | the `task` command for a better experience developing in this repository. 62 | 63 | The task `fmt-and-lint` can be used to check the formatting and lint your code to ensure it 64 | fits with the rest of the repository. 65 | 66 | In VSCode, press `F5` to build and debug the VSCode extension. 67 | 68 | ### Architecture 69 | 70 | The server itself is implemented in Rust using `tower-lsp`. It communicates with 71 | editors via JSON-RPC through standard input/output, according to the language 72 | server protocol. 73 | 74 | ### Contributing 75 | 76 | We appreciate contributions! I recommend reaching out on Discord (the invite to 77 | which can be found at [pest.rs](https://pest.rs)) before contributing, to check 78 | with us. 79 | 80 | ## Credits 81 | 82 | - [OsoHQ](https://github.com/osohq), for their 83 | [blog post](https://www.osohq.com/post/building-vs-code-extension-with-rust-wasm-typescript), 84 | and open source code which was used as inspiration. 85 | - [Stef Gijsberts](https://github.com/Stef-Gijsberts) for their 86 | [Pest syntax highlighting TextMate bundle](https://github.com/Stef-Gijsberts/pest-Syntax-Highlighting-for-vscode) 87 | which is used in this extension under the MIT license. 88 | -------------------------------------------------------------------------------- /vscode/client/src/index.ts: -------------------------------------------------------------------------------- 1 | import { findServer } from "./server"; 2 | import { 3 | commands, 4 | ExtensionContext, 5 | RelativePattern, 6 | TextDocument, 7 | Uri, 8 | window, 9 | workspace, 10 | WorkspaceFolder, 11 | WorkspaceFoldersChangeEvent, 12 | } from "vscode"; 13 | import { LanguageClient } from "vscode-languageclient/node"; 14 | 15 | const extensionName = "Pest Language Server"; 16 | export const outputChannel = window.createOutputChannel(extensionName); 17 | 18 | const clients: Map = new Map(); 19 | 20 | function pestFilesInFolderPattern(folder: Uri) { 21 | return new RelativePattern(folder, "**/*.pest"); 22 | } 23 | 24 | async function openPestFilesInFolder(folder: Uri) { 25 | const pattern = pestFilesInFolderPattern(folder); 26 | const uris = await workspace.findFiles(pattern); 27 | return Promise.all(uris.map(openDocument)); 28 | } 29 | 30 | async function openDocument(uri: Uri) { 31 | const uriMatch = (d: TextDocument) => d.uri.toString() === uri.toString(); 32 | const doc = workspace.textDocuments.find(uriMatch); 33 | if (doc === undefined) await workspace.openTextDocument(uri); 34 | return uri; 35 | } 36 | 37 | async function startClientsForFolder( 38 | folder: WorkspaceFolder, 39 | ctx: ExtensionContext 40 | ) { 41 | const command = await findServer(); 42 | 43 | if (!command) { 44 | outputChannel.appendLine("[TS] Aborting server start."); 45 | await window.showErrorMessage( 46 | "Not starting Pest Language Server as a suitable binary was not found." 47 | ); 48 | return; 49 | } 50 | 51 | const customArgs = workspace 52 | .getConfiguration("pestIdeTools") 53 | .get("customArgs") as string[]; 54 | 55 | const root = folder.uri; 56 | const pestFiles: Set = new Set(); 57 | 58 | const deleteWatcher = workspace.createFileSystemWatcher( 59 | pestFilesInFolderPattern(root), 60 | true, // ignoreCreateEvents 61 | true, // ignoreChangeEvents 62 | false // ignoreDeleteEvents 63 | ); 64 | 65 | const createChangeWatcher = workspace.createFileSystemWatcher( 66 | pestFilesInFolderPattern(root), 67 | false, // ignoreCreateEvents 68 | false, // ignoreChangeEvents 69 | true // ignoreDeleteEvents 70 | ); 71 | 72 | ctx.subscriptions.push(deleteWatcher); 73 | ctx.subscriptions.push(createChangeWatcher); 74 | 75 | const client = new LanguageClient( 76 | extensionName, 77 | { 78 | command, 79 | args: customArgs, 80 | }, 81 | { 82 | documentSelector: [ 83 | { language: "pest", pattern: `${root.fsPath}/**/*.pest` }, 84 | ], 85 | synchronize: { fileEvents: deleteWatcher }, 86 | diagnosticCollectionName: extensionName, 87 | workspaceFolder: folder, 88 | outputChannel, 89 | } 90 | ); 91 | 92 | ctx.subscriptions.push(client.start()); 93 | ctx.subscriptions.push(createChangeWatcher.onDidCreate(openDocument)); 94 | ctx.subscriptions.push(createChangeWatcher.onDidChange(openDocument)); 95 | 96 | const openedFiles = await openPestFilesInFolder(root); 97 | openedFiles.forEach(f => pestFiles.add(f.toString())); 98 | clients.set(root.toString(), client); 99 | } 100 | 101 | function stopClient(client: LanguageClient) { 102 | client.diagnostics?.clear(); 103 | return client.stop(); 104 | } 105 | 106 | async function stopClientsForFolder(workspaceFolder: string) { 107 | const client = clients.get(workspaceFolder); 108 | if (client) { 109 | await stopClient(client); 110 | } 111 | 112 | clients.delete(workspaceFolder); 113 | } 114 | 115 | function updateClients(context: ExtensionContext) { 116 | return async function ({ added, removed }: WorkspaceFoldersChangeEvent) { 117 | for (const folder of removed) { 118 | await stopClientsForFolder(folder.uri.toString()); 119 | } 120 | 121 | for (const folder of added) { 122 | await startClientsForFolder(folder, context); 123 | } 124 | }; 125 | } 126 | 127 | export async function activate(context: ExtensionContext): Promise { 128 | const folders = workspace.workspaceFolders || []; 129 | 130 | for (const folder of folders) { 131 | await startClientsForFolder(folder, context); 132 | } 133 | 134 | context.subscriptions.push( 135 | workspace.onDidChangeWorkspaceFolders(updateClients(context)) 136 | ); 137 | 138 | context.subscriptions.push( 139 | workspace.onDidChangeConfiguration(async e => { 140 | if (e.affectsConfiguration("pestIdeTools.serverPath")) { 141 | for (const client of clients.values()) { 142 | const folder = client.clientOptions.workspaceFolder; 143 | await stopClient(client); 144 | 145 | if (folder) { 146 | await startClientsForFolder(folder, context); 147 | } 148 | } 149 | } else { 150 | for (const client of clients.values()) { 151 | client.sendNotification("workspace/didChangeConfiguration", { 152 | settings: { 153 | ...workspace.getConfiguration("pestIdeTools"), 154 | checkForUpdates: false, 155 | }, 156 | }); 157 | } 158 | } 159 | }) 160 | ); 161 | 162 | commands.registerCommand("pestIdeTools.restartServer", async () => { 163 | const currentFolder = workspace.workspaceFolders?.[0].uri.toString(); 164 | 165 | if (!currentFolder) { 166 | await window.showInformationMessage("No Pest workspace is open."); 167 | return; 168 | } 169 | 170 | const client = clients.get(currentFolder); 171 | 172 | if (client) { 173 | const folder = client.clientOptions.workspaceFolder; 174 | await stopClient(client); 175 | 176 | if (folder) { 177 | await startClientsForFolder(folder, context); 178 | } 179 | } 180 | }); 181 | } 182 | 183 | export async function deactivate(): Promise { 184 | await Promise.all([...clients.values()].map(stopClient)); 185 | } 186 | -------------------------------------------------------------------------------- /vscode/client/src/server.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-call */ 2 | // eslint-disable-next-line prettier/prettier 3 | 4 | /* eslint-disable @typescript-eslint/no-unsafe-argument */ 5 | 6 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */ 7 | import { outputChannel } from "."; 8 | import { exec, ExecException, spawn } from "child_process"; 9 | import { stat } from "fs/promises"; 10 | import fetch, { AbortError } from "node-fetch"; 11 | import path, { join } from "path"; 12 | import { promisify } from "util"; 13 | import { Progress, ProgressLocation, window, workspace } from "vscode"; 14 | 15 | export async function findServer(): Promise { 16 | const path = await findServerPath(); 17 | 18 | if (!path || !(await checkValidity(path))) { 19 | return undefined; 20 | } 21 | 22 | outputChannel.appendLine(`[TS] Using server path ${path}.`); 23 | 24 | const config = workspace.getConfiguration("pestIdeTools"); 25 | const updateCheckerEnabled = config.get("checkForUpdates") as boolean; 26 | 27 | // use quotes around path because the path may have spaces in it 28 | const currentVersion = await promisify(exec)(`"${path}" --version`).then(s => 29 | s.stdout.trim() 30 | ); 31 | outputChannel.appendLine(`[TS] Server version: v${currentVersion}`); 32 | 33 | if (updateCheckerEnabled && config.get("serverPath") === null) { 34 | outputChannel.appendLine("[TS] Checking for updates..."); 35 | 36 | try { 37 | const abortController = new AbortController(); 38 | const timeout = setTimeout(() => abortController.abort(), 2500); 39 | let res; 40 | 41 | try { 42 | res = await fetch( 43 | "https://crates.io/api/v1/crates/pest-language-server", 44 | { signal: abortController.signal } 45 | ); 46 | res = await res.json(); 47 | } catch (e) { 48 | if (e instanceof AbortError) { 49 | outputChannel.appendLine("[TS] Update check timed out after 2000ms."); 50 | return path; 51 | } else { 52 | throw e; 53 | } 54 | } finally { 55 | clearTimeout(timeout); 56 | } 57 | 58 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 59 | // @ts-ignore 60 | const latestVersion = res["crate"]["max_version"] as string; 61 | 62 | if (currentVersion !== latestVersion) { 63 | const choice = await window.showInformationMessage( 64 | `A new version of the Pest Language Server is available (v${currentVersion} --> v${latestVersion}). Would you like to update automatically?`, 65 | {}, 66 | "Yes" 67 | ); 68 | 69 | if (choice) { 70 | if (!(await installBinaryViaCargoInstall())) { 71 | await window.showErrorMessage( 72 | "Failed to update Pest Language Server." 73 | ); 74 | } 75 | } 76 | } 77 | } catch (_) { 78 | outputChannel.appendLine("[TS] Failed to run update check."); 79 | } 80 | } 81 | 82 | return path; 83 | } 84 | 85 | async function findServerPath(): Promise { 86 | const config = workspace.getConfiguration("pestIdeTools"); 87 | 88 | // Check for custom server path 89 | if (config.get("serverPath") && workspace.workspaceFolders !== undefined) { 90 | outputChannel.appendLine( 91 | path.resolve( 92 | workspace.workspaceFolders[0].uri.fsPath, 93 | config.get("serverPath") as string 94 | ) 95 | ); 96 | return path.resolve( 97 | workspace.workspaceFolders[0].uri.fsPath, 98 | config.get("serverPath") as string 99 | ); 100 | } 101 | 102 | const cargoBinDirectory = getCargoBinDirectory(); 103 | 104 | if (!cargoBinDirectory) { 105 | outputChannel.appendLine("[TS] Could not find cargo bin directory."); 106 | return undefined; 107 | } 108 | 109 | const expectedPath = join(cargoBinDirectory, getExpectedBinaryName()); 110 | outputChannel.appendLine(`[TS] Trying path ${expectedPath}...`); 111 | 112 | if (await checkValidity(expectedPath)) { 113 | return expectedPath; 114 | } 115 | 116 | const choice = await window.showWarningMessage( 117 | "Failed to find an installed Pest Language Server. Would you like to install one using `cargo install`?", 118 | {}, 119 | "Yes" 120 | ); 121 | 122 | if (!choice) { 123 | outputChannel.appendLine("[TS] Not installing server."); 124 | return undefined; 125 | } 126 | 127 | if (await installBinaryViaCargoInstall()) { 128 | return expectedPath; 129 | } else { 130 | await window.showErrorMessage( 131 | "Failed to install Pest Language Server. Please either run `cargo install pest-language-server`, or set a custom path using the configuration `pestIdeTools.serverPath`." 132 | ); 133 | } 134 | 135 | return undefined; 136 | } 137 | 138 | function getCargoBinDirectory(): string | undefined { 139 | const cargoInstallRoot = process.env["CARGO_INSTALL_ROOT"]; 140 | 141 | if (cargoInstallRoot) { 142 | return cargoInstallRoot; 143 | } 144 | 145 | const cargoHome = process.env["CARGO_HOME"]; 146 | 147 | if (cargoHome) { 148 | return join(cargoHome, "bin"); 149 | } 150 | 151 | let home = process.env["HOME"]; 152 | 153 | if (process.platform === "win32") { 154 | home = process.env["USERPROFILE"]; 155 | } 156 | 157 | if (home) { 158 | return join(home, ".cargo", "bin"); 159 | } 160 | 161 | return undefined; 162 | } 163 | 164 | function getExpectedBinaryName(): string { 165 | switch (process.platform) { 166 | case "win32": 167 | return "pest-language-server.exe"; 168 | default: 169 | return "pest-language-server"; 170 | } 171 | } 172 | 173 | async function checkValidity(path: string): Promise { 174 | try { 175 | await stat(path); 176 | return true; 177 | } catch (_) { 178 | return false; 179 | } 180 | } 181 | 182 | async function installBinaryViaCargoInstall(): Promise { 183 | outputChannel.appendLine("[TS] Installing server."); 184 | 185 | return await window.withProgress( 186 | { 187 | location: ProgressLocation.Notification, 188 | cancellable: true, 189 | title: "Installing Pest Language Server", 190 | }, 191 | async (progress, token) => { 192 | try { 193 | progress.report({ message: "spawning `cargo install` command" }); 194 | 195 | const process = spawn("cargo", ["install", "pest-language-server"], { 196 | shell: true, 197 | }); 198 | 199 | token.onCancellationRequested(() => { 200 | process.kill(); 201 | }); 202 | 203 | process.stderr.on("data", data => 204 | logCargoInstallProgress(data.toString(), progress) 205 | ); 206 | 207 | process.stdout.on("data", data => 208 | logCargoInstallProgress(data.toString(), progress) 209 | ); 210 | 211 | const exitCode: number = await new Promise((resolve, _) => { 212 | process.on("close", resolve); 213 | }); 214 | 215 | outputChannel.appendLine( 216 | `[TS]: Cargo process exited with code ${exitCode}` 217 | ); 218 | 219 | if (exitCode === 0) { 220 | progress.report({ increment: 100, message: "installed" }); 221 | 222 | return true; 223 | } else { 224 | throw new Error(`Received non-zero exit code: ${exitCode}`); 225 | } 226 | } catch (e) { 227 | outputChannel.appendLine(`[TS] ${(e as ExecException).message}`); 228 | progress.report({ message: "An error occurred." }); 229 | return false; 230 | } 231 | } 232 | ); 233 | } 234 | 235 | function logCargoInstallProgress( 236 | data: string, 237 | progress: Progress<{ 238 | message?: string | undefined; 239 | increment?: number | undefined; 240 | }> 241 | ) { 242 | data = data.trim(); 243 | 244 | let msg; 245 | const versionRegex = /v[0-9]+\.[0-9]+\.[0-9]+/; 246 | 247 | if (data === "Updating crates.io index") { 248 | msg = "updating crates.io index"; 249 | } else if ( 250 | data === 251 | "Updating git repository `https://github.com/pest-parser/pest-ide-tools`" 252 | ) { 253 | msg = "fetching crate"; 254 | } else if (data.startsWith("Compiling")) { 255 | msg = `compiling ${data.split("Compiling ")[1].split(versionRegex)[0]}`; 256 | } 257 | 258 | if (msg) { 259 | outputChannel.appendLine(`[TS] \t${msg}`); 260 | progress.report({ message: `\t${msg}` }); 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /vscode/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": [ 4 | "src" 5 | ], 6 | "files": [ 7 | "../package.json" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /vscode/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pest-parser/pest-ide-tools/6344e2a0fdb4af42dfdc106980fe730b818204c5/vscode/icon.png -------------------------------------------------------------------------------- /vscode/language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | "lineComment": "//", 4 | "blockComment": ["/*", "*/"] 5 | }, 6 | "autoClosingPairs": [ 7 | { 8 | "open": "{", 9 | "close": "}", 10 | "notIn": ["string"] 11 | }, 12 | { 13 | "open": "[", 14 | "close": "]", 15 | "notIn": ["string"] 16 | }, 17 | { 18 | "open": "(", 19 | "close": ")", 20 | "notIn": ["string"] 21 | }, 22 | { 23 | "open": "\"", 24 | "close": "\"", 25 | "notIn": ["string"] 26 | }, 27 | { 28 | "open": "'", 29 | "close": "'", 30 | "notIn": ["string"] 31 | }, 32 | { 33 | "open": "/*", 34 | "close": "*/", 35 | "notIn": ["string"] 36 | } 37 | ], 38 | "autoCloseBefore": ";:.,=}])>` \n\t", 39 | "surroundingPairs": [ 40 | ["{", "}"], 41 | ["[", "]"], 42 | ["(", ")"], 43 | ["\"", "\""], 44 | ["'", "'"] 45 | ], 46 | "colorizedBracketPairs": [ 47 | ["(", ")"], 48 | ["[", "]"], 49 | ["{", "}"] 50 | ], 51 | "folding": { 52 | "markers": { 53 | "start": "^.+\\s=\\s[@_#!]?{\\n", 54 | "end": "^([^\"][^\\s\\S])*}([^\"][\\s\\S])*" 55 | }, 56 | "offSide": false 57 | }, 58 | "indentationRules": { 59 | "increaseIndentPattern": "^((?!\\/(\\/|\\*)).)*(\\{[^}\"'`]*|\\([^)\"'`]*|\\[[^\\]\"'`]*)$", 60 | "decreaseIndentPattern": "^((?!.*?\\/\\*).*\\*/)?\\s*[\\)\\}\\]].*$" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /vscode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pest-ide-tools", 3 | "displayName": "Pest IDE Tools", 4 | "description": "Tools for the Rust-based Pest parser generator", 5 | "publisher": "pest", 6 | "homepage": "https://pest.rs", 7 | "keywords": [ 8 | "pest", 9 | "parser", 10 | "rust", 11 | "peg", 12 | "grammar" 13 | ], 14 | "categories": [ 15 | "Formatters", 16 | "Programming Languages" 17 | ], 18 | "version": "0.3.11", 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/pest-parser/pest-ide-tools" 22 | }, 23 | "bugs": { 24 | "url": "https://github.com/pest-parser/pest-ide-tools/issues" 25 | }, 26 | "license": "Apache-2.0", 27 | "icon": "icon.png", 28 | "engines": { 29 | "vscode": "^1.74.0" 30 | }, 31 | "main": "./build/client.js", 32 | "activationEvents": [ 33 | "onStartupFinished" 34 | ], 35 | "contributes": { 36 | "languages": [ 37 | { 38 | "id": "pest", 39 | "aliases": [ 40 | "Pest", 41 | "pest" 42 | ], 43 | "extensions": [ 44 | ".pest" 45 | ], 46 | "configuration": "./language-configuration.json", 47 | "icon": { 48 | "light": "icon.png", 49 | "dark": "icon.png" 50 | } 51 | } 52 | ], 53 | "grammars": [ 54 | { 55 | "language": "pest", 56 | "scopeName": "source.pest", 57 | "path": "./syntaxes/pest.tmLanguage.json" 58 | } 59 | ], 60 | "configuration": [ 61 | { 62 | "id": "pestIdeTools", 63 | "title": "Pest IDE Tools", 64 | "properties": { 65 | "pestIdeTools.serverPath": { 66 | "type": [ 67 | "string", 68 | "null" 69 | ], 70 | "default": null, 71 | "description": "Set a custom path to a Pest LS binary.", 72 | "scope": "window" 73 | }, 74 | "pestIdeTools.customArgs": { 75 | "type": "array", 76 | "description": "Additional arguments that should be passed to the Pest LS binary.", 77 | "default": [], 78 | "scope": "window" 79 | }, 80 | "pestIdeTools.checkForUpdates": { 81 | "type": "boolean", 82 | "description": "Check for updates to the Pest LS binary.", 83 | "default": true, 84 | "scope": "window" 85 | }, 86 | "pestIdeTools.alwaysUsedRuleNames": { 87 | "type": "array", 88 | "description": "Rule names that should not be included in the unused rule names diagnostic.", 89 | "default": [], 90 | "scope": "window" 91 | } 92 | } 93 | } 94 | ], 95 | "commands": [ 96 | { 97 | "category": "Pest", 98 | "command": "pestIdeTools.restartServer", 99 | "title": "Restart Server" 100 | } 101 | ] 102 | }, 103 | "scripts": { 104 | "package": "vsce package --githubBranch main --out pest.vsix", 105 | "publish:vsce": "vsce publish --githubBranch main", 106 | "publish:ovsx": "ovsx publish -p $OPENVSX_PAT", 107 | "fix": "gts fix", 108 | "lint": "gts lint -- . --max-warnings 0", 109 | "fmt": "prettier --write \"client/**/*.ts\" \"*.js\" \"language-configuration.json\" \"tsconfig.json\" \"syntaxes/*.json\"", 110 | "fmt-check": "prettier --check \"client/**/*.ts\" \"*.js\" \"language-configuration.json\" \"tsconfig.json\" \"syntaxes/*.json\"", 111 | "esbuild-with-rust": "cd .. && cargo build -p pest-language-server && cd vscode && npm run esbuild-client", 112 | "esbuild-client": "esbuild client=./client/src --bundle --outdir=build --external:vscode --format=cjs --platform=node --minify", 113 | "vscode:prepublish": "npm run esbuild-client" 114 | }, 115 | "dependencies": { 116 | "node-fetch": "^3.3.2", 117 | "vscode-languageclient": "^7.0.0" 118 | }, 119 | "devDependencies": { 120 | "@trivago/prettier-plugin-sort-imports": "^4.3.0", 121 | "@types/node": "^18.15.12", 122 | "@types/vscode": "^1.74.0", 123 | "@typescript-eslint/eslint-plugin": "^7.12.0", 124 | "@typescript-eslint/parser": "^7.12.0", 125 | "@vscode/vsce": "^2.27.0", 126 | "esbuild": "^0.21.5", 127 | "eslint-config-prettier": "^9.0.0", 128 | "eslint-plugin-node": "^11.1.0", 129 | "eslint-plugin-prettier": "^5.0.0", 130 | "gts": "^5.3.0", 131 | "ovsx": "^0.9.1", 132 | "typescript": "^5.4.5" 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /vscode/syntaxes/pest.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", 3 | "name": "Pest", 4 | "patterns": [ 5 | { 6 | "include": "#keywords" 7 | }, 8 | { 9 | "include": "#strings" 10 | }, 11 | { 12 | "include": "#tags" 13 | }, 14 | { 15 | "include": "#comments" 16 | }, 17 | { 18 | "include": "#multilinecomments" 19 | } 20 | ], 21 | "repository": { 22 | "keywords": { 23 | "patterns": [ 24 | { 25 | "name": "keyword.control.pest", 26 | "match": "\\b(PUSH|POP|PEEK|POP_ALL|PEEK_ALL)\\b" 27 | }, 28 | { 29 | "name": "variable.language.implicit.pest", 30 | "match": "\\b(WHITESPACE|COMMENT)\\b" 31 | }, 32 | { 33 | "name": "constant.language.borders.pest", 34 | "match": "\\b(SOI|EOI)\\b" 35 | }, 36 | { 37 | "name": "constant.language.character.ascii.pest", 38 | "match": "\\b(NEWLINE|ASCII_DIGIT|ASCII_NONZERO_DIGIT|ASCII_BIN_DIGIT|ASCII_OCT_DIGIT|ASCII_HEX_DIGIT|ASCII_ALPHA_LOWER|ASCII_ALPHA_UPPER|ASCII_ALPHA|ASCII_ALPHANUMERIC)\\b" 39 | }, 40 | { 41 | "name": "constant.language.character.unicode.letter.pest", 42 | "match": "\\b(ANY|LETTER|CASED_LETTER|UPPERCASE_LETTER|LOWERCASE_LETTER|TITLECASE_LETTER|MODIFIER_LETTER|OTHER_LETTER)\\b" 43 | }, 44 | { 45 | "name": "constant.language.character.unicode.mark.pest", 46 | "match": "\\b(MARK|COMBINING_SPACING_MARK|ENCLOSING_MARK|NONSPACING_MARK)\\b" 47 | }, 48 | { 49 | "name": "constant.language.character.unicode.number.pest", 50 | "match": "\\b(NUMBER|DECIMAL_NUMBER|LETTER_NUMBER|OTHER_NUMBER)\\b" 51 | }, 52 | { 53 | "name": "constant.language.character.unicode.punctuation.pest", 54 | "match": "\\b(PUNCTUATION|CONNECTOR_PUNCTUATION|DASH_PUNCTUATION|OPEN_PUNCTUATION|CLOSE_PUNCTUATION|INITIAL_PUNCTUATION|FINAL_PUNCTUATION|OTHER_PUNCTUATION)\\b" 55 | }, 56 | { 57 | "name": "constant.language.character.unicode.symbol.pest", 58 | "match": "\\b(SYMBOL|MATH_SYMBOL|CURRENCY_SYMBOL|MODIFIER_SYMBOL|OTHER_SYMBOL)\\b" 59 | }, 60 | { 61 | "name": "constant.language.character.unicode.separator.pest", 62 | "match": "\\b(SEPARATOR|SPACE_SEPARATOR|LINE_SEPARATOR|PARAGRAPH_SEPARATOR)\\b" 63 | }, 64 | { 65 | "name": "constant.language.character.unicode.other.pest", 66 | "match": "\\b(CONTROL|FORMAT|SURROGATE|PRIVATE_USE|UNASSIGNED)\\b" 67 | }, 68 | { 69 | "name": "constant.language.character.unicode.binary.pest", 70 | "match": "\\b(ALPHABETIC|BIDI_CONTROL|CASE_IGNORABLE|CASED|CHANGES_WHEN_CASEFOLDED|CHANGES_WHEN_CASEMAPPED|CHANGES_WHEN_LOWERCASED|CHANGES_WHEN_TITLECASED|CHANGES_WHEN_UPPERCASED|DASH|DEFAULT_IGNORABLE_CODE_POINT|DEPRECATED|DIACRITIC|EXTENDER|GRAPHEME_BASE|GRAPHEME_EXTEND|GRAPHEME_LINK|HEX_DIGIT|HYPHEN|IDS_BINARY_OPERATOR|IDS_TRINARY_OPERATOR|ID_CONTINUE|ID_START|IDEOGRAPHIC|JOIN_CONTROL|LOGICAL_ORDER_EXCEPTION|LOWERCASE|MATH|NONCHARACTER_CODE_POINT|OTHER_ALPHABETIC|OTHER_DEFAULT_IGNORABLE_CODE_POINT|OTHER_GRAPHEME_EXTEND|OTHER_ID_CONTINUE|OTHER_ID_START|OTHER_LOWERCASE|OTHER_MATH|OTHER_UPPERCASE|PATTERN_SYNTAX|PATTERN_WHITE_SPACE|PREPENDED_CONCATENATION_MARK|QUOTATION_MARK|RADICAL|REGIONAL_INDICATOR|SENTENCE_TERMINAL|SOFT_DOTTED|TERMINAL_PUNCTUATION|UNIFIED_IDEOGRAPH|UPPERCASE|VARIATION_SELECTOR|WHITE_SPACE|XID_CONTINUE|XID_START)\\b" 71 | }, 72 | { 73 | "name": "keyword.operator.pest", 74 | "match": "-|\\.\\.|!|~|_|@|\\$" 75 | }, 76 | { 77 | "name": "keyword.operator.repetition.pest", 78 | "match": "\\+|\\*|\\?|{\\s*[0-9]+\\s*,\\s*}|{\\s*,\\s*[0-9]+\\s*}|{\\s*[0-9]+\\s*,\\s*[0-9]+\\s*}" 79 | }, 80 | { 81 | "name": "variable.name.pest", 82 | "match": "\b[A-Za-z_][A-Za-z0-9_]*\b" 83 | } 84 | ] 85 | }, 86 | "strings": { 87 | "patterns": [ 88 | { 89 | "name": "string.quoted.double.pest", 90 | "begin": "\"", 91 | "end": "\"", 92 | "patterns": [ 93 | { 94 | "include": "#characterescape" 95 | } 96 | ] 97 | }, 98 | { 99 | "name": "string.quoted.single.pest", 100 | "begin": "'", 101 | "end": "'", 102 | "patterns": [ 103 | { 104 | "include": "#characterescape" 105 | } 106 | ] 107 | } 108 | ] 109 | }, 110 | "tags": { 111 | "patterns": [ 112 | { 113 | "name": "entity.name.tag.pest", 114 | "match": "#[A-Za-z_][A-Za-z0-9_]*" 115 | }, 116 | { 117 | "name": "keyword.operator.assignment.pest", 118 | "match": "=" 119 | } 120 | ] 121 | }, 122 | "comments": { 123 | "patterns": [ 124 | { 125 | "name": "comment.line.pest", 126 | "match": "//.*" 127 | } 128 | ] 129 | }, 130 | "multilinecomments": { 131 | "patterns": [ 132 | { 133 | "name": "comment.block.pest", 134 | "begin": "/\\*", 135 | "end": "\\*/", 136 | "patterns": [ 137 | { 138 | "include": "#multilinecomments" 139 | } 140 | ] 141 | } 142 | ] 143 | }, 144 | "characterescape": { 145 | "name": "constant.character.escape.pest", 146 | "match": "((\\\\u\\{[a-fA-F0-9]{4}\\})|(\\\\.))" 147 | } 148 | }, 149 | "scopeName": "source.pest" 150 | } 151 | -------------------------------------------------------------------------------- /vscode/tests/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "pestIdeTools.serverPath": "../../target/debug/pest-language-server" 3 | } 4 | -------------------------------------------------------------------------------- /vscode/tests/cjk.pest: -------------------------------------------------------------------------------- 1 | // A test for CJK/non-ascii characters characters 2 | 3 | root = { inner+ ~ "中文" ~ ANY } 4 | /// No idea what these characters mean, just random ones. 5 | inner = { "文" | "中" } 6 | -------------------------------------------------------------------------------- /vscode/tests/json.pest: -------------------------------------------------------------------------------- 1 | //! A parser for JSON file. 2 | //! 3 | //! And this is a example for JSON parser. 4 | json = { SOI ~ (object | array) ~ EOI } 5 | 6 | /// Matches object, e.g.: `{ "foo": "bar" }` 7 | /// Foobar 8 | object = { "{" ~ pair ~ ("," ~ pair)* ~ "}" | "{" ~ "}" } 9 | pair = { string ~ ":" ~ value } 10 | 11 | array = { "[" ~ value ~ ("," ~ value)* ~ "]" | "[" ~ "]" } 12 | 13 | /* Match value */ 14 | value = { string | number | object | array | bool | null } 15 | 16 | string = @{ "\"" ~ inner ~ "\"" } 17 | inner = @{ (!("\"" | "\\") ~ ANY)* ~ (escape ~ inner)? } 18 | escape = @{ "\\" ~ ("\"" | "\\" | "/" | "b" | "f" | "n" | "r" | "t" | unicode) } 19 | // Unicode, e.g.: `u0000` 20 | unicode = @{ "u" ~ ASCII_HEX_DIGIT{4} } 21 | 22 | /// int and float, including nagative number 23 | number = @{ "-"? ~ int ~ ("." ~ ASCII_DIGIT+ ~ exp? | exp)? } 24 | int = @{ "0" | ASCII_NONZERO_DIGIT ~ ASCII_DIGIT* } 25 | exp = @{ ("E" | "e") ~ ("+" | "-")? ~ ASCII_DIGIT+ } 26 | 27 | bool = { "true" | "false" } 28 | 29 | null = { "null" } 30 | 31 | WHITESPACE = _{ " " | "\t" | "\r" | "\n" } 32 | -------------------------------------------------------------------------------- /vscode/tests/misc.pest: -------------------------------------------------------------------------------- 1 | test_range = { 'a'..'z' } 2 | tagged_rule = { #tag_name = test_range+ } 3 | -------------------------------------------------------------------------------- /vscode/tests/no_consume.pest: -------------------------------------------------------------------------------- 1 | buggy = { (!"a")* } 2 | -------------------------------------------------------------------------------- /vscode/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "esModuleInterop": true, 5 | "isolatedModules": true, 6 | "lib": ["ES2020"], 7 | "module": "commonjs", 8 | "outDir": "build", 9 | "resolveJsonModule": true, 10 | "rootDir": ".", 11 | "sourceMap": true, 12 | "target": "ES2020" 13 | }, 14 | "exclude": ["./node_modules", "./build"], 15 | "extends": "./node_modules/gts/tsconfig-google.json", 16 | "include": [], 17 | "references": [ 18 | { 19 | "path": "./client" 20 | } 21 | ] 22 | } 23 | --------------------------------------------------------------------------------