├── .github └── workflows │ └── build.yml ├── .gitignore ├── .maintainers_guide.md ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.md ├── NOTICE.md ├── README.md ├── SECURITY.md ├── build └── notice-file-generate.js ├── docs ├── debugging.md └── images │ └── debugger-setups.png ├── hack.configuration.json ├── images ├── logo.png └── logo.svg ├── package-lock.json ├── package.json ├── snippets └── hack.json ├── src ├── Config.ts ├── HhvmDebugConfigurationProvider.ts ├── LSPHHASTLint.ts ├── LSPHackTypeChecker.ts ├── LegacyHackTypeChecker.ts ├── Utils.ts ├── coveragechecker.ts ├── debugger.ts ├── main.ts ├── providers.ts ├── proxy.ts ├── remote.ts ├── test │ ├── runTest.ts │ └── suite │ │ ├── extension.test.ts │ │ └── index.ts └── types │ ├── hack.d.ts │ └── lsp.d.ts ├── syntaxes ├── README.md ├── codeblock.json ├── hack.json └── test │ ├── .hhconfig │ ├── README.md │ ├── abstract_final.hack │ ├── async.hack │ ├── basic_types.hack │ ├── bools.hack │ ├── collections.hack │ ├── default_param_new_line.hack │ ├── double_greater_than.hack │ ├── enum_classes.hack │ ├── enums.hack │ ├── generic_shape_type.hack │ ├── interpolation.hack │ ├── modules.hack │ ├── namespaces.hack │ ├── order_of_attributes.hack │ ├── shape_constructor.hack │ ├── unpaired_apostrophe.hack │ └── xhp_with_apostrophe.hack └── tsconfig.json /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | branches: 5 | - master 6 | tags: 7 | - v* 8 | pull_request: 9 | branches: 10 | - master 11 | 12 | jobs: 13 | build: 14 | strategy: 15 | matrix: 16 | os: [macos-latest, ubuntu-latest] 17 | runs-on: ${{ matrix.os }} 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | - name: Install Node.js 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: 20.x 25 | - run: npm ci 26 | - run: xvfb-run -a npm test 27 | if: runner.os == 'Linux' 28 | - run: npm test 29 | if: runner.os != 'Linux' 30 | - run: npm run package 31 | - uses: svenstaro/upload-release-action@2.2.1 32 | if: | 33 | matrix.os == 'ubuntu-latest' && 34 | startsWith(github.ref, 'refs/tags/v') 35 | with: 36 | repo_token: ${{ secrets.GITHUB_TOKEN }} 37 | file: "*.vsix" 38 | file_glob: true 39 | tag: ${{ github.ref }} 40 | 41 | #- name: Publish 42 | # if: success() && startsWith( github.ref, 'refs/tags/releases/') && matrix.os == 'ubuntu-latest' 43 | # run: npm run deploy 44 | # env: 45 | # VSCE_PAT: ${{ secrets.VSCE_PAT }} 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | .vscode-test 4 | npm-debug.log -------------------------------------------------------------------------------- /.maintainers_guide.md: -------------------------------------------------------------------------------- 1 | # Maintainers Guide 2 | 3 | This document describes tools, tasks and workflow that one needs to be familiar 4 | with in order to effectively maintain this project. If you use this package 5 | within your own software as is but don't plan on modifying it, this guide is 6 | **not** for you. 7 | 8 | ## Tasks 9 | 10 | ### Testing 11 | 12 | Run unit tests locally using the `npm test` command. Tests are also run 13 | automatically on all branches using 14 | [Travis CI](https://travis-ci.org/slackhq/vscode-hack). 15 | 16 | ### Releasing 17 | 18 | This extension is published at 19 | https://marketplace.visualstudio.com/items?itemName=pranayagarwal.vscode-hack. 20 | 21 | To push a new release, follow these steps: 22 | 23 | 1. Make sure the `master` branch is up to date with all changes and has been 24 | tested. 25 | 2. Merge a new commit with the following changes: 26 | - Update the version in `package.json` by following the versioning guide 27 | below 28 | - Re-run `npm install` to update package-lock file version and regenerate 29 | `NOTICE.md` 30 | - Add a description of all changes since the last release in `CHANGELOG.md` 31 | - Add or update the "Latest releases" section in `README.md` with release 32 | highlights 33 | 3. Draft a new GitHub release: 34 | - Releases should always target the `master` branch 35 | - Tag version and release title should both be in the format "v1.2.3", with 36 | the version matching the value in `package.json` 37 | - Copy your new section from `CHANGELOG.md` in the release description 38 | 4. Once the release is published, a new Github build will automatically start. 39 | Ensure that the build passes and a new `vscode-hack-[version].vsix` is added 40 | as an asset to the GitHub release page. 41 | 5. Publish the new version to the VS Code Marketplace. This can only be done 42 | from Pranay Agarwal's Microsoft account. 43 | 44 | ## Workflow 45 | 46 | ### Versioning and Tags 47 | 48 | Even though this project is not published as an npm package, it uses a rough 49 | form of semver for versioning. Increment the MAJOR or MINOR version for feature 50 | additions, depending on size and impact, and PATCH version for bug fixes. 51 | 52 | Releases are tagged in git and published on the repository releases page on 53 | GitHub. 54 | 55 | ### Branches 56 | 57 | All development should happen in feature branches. `master` should be ready for 58 | quick patching and publishing at all times. 59 | 60 | ### Issue Management 61 | 62 | Labels are used to run issues through an organized workflow. Here are the basic 63 | definitions: 64 | 65 | - `bug`: A confirmed bug report. A bug is considered confirmed when reproduction 66 | steps have been documented and the issue has been reproduced. 67 | - `enhancement` or `feature request`: A feature request for something this 68 | package might not already do. 69 | - `docs`: An issue that is purely about documentation work. 70 | - `tests`: An issue that is purely about testing work. 71 | - `needs feedback`: An issue that may have claimed to be a bug but was not 72 | reproducible, or was otherwise missing some information. 73 | - `discussion`: An issue that is purely meant to hold a discussion. Typically 74 | the maintainers are looking for feedback in this issues. 75 | - `question`: An issue that is like a support request because the user's usage 76 | was not correct. 77 | - `semver:major|minor|patch`: Metadata about how resolving this issue would 78 | affect the version number. 79 | - `security`: An issue that has special consideration for security reasons. 80 | - `good first contribution`: An issue that has a well-defined relatively-small 81 | scope, with clear expectations. It helps when the testing approach is also 82 | known. 83 | - `duplicate`: An issue that is functionally the same as another issue. Apply 84 | this only if you've linked the other issue by number. 85 | - `external`: An issue that is caused by an external dependency and cannot be 86 | fixed here. 87 | 88 | **Triage** is the process of taking new issues that aren't yet "seen" and 89 | marking them with a basic level of information with labels. An issue should have 90 | **one** of the following labels applied: `bug`, `enhancement`, `question`, 91 | `needs feedback`, `docs`, `tests`, or `discussion`. 92 | 93 | Issues are closed when a resolution has been reached. If for any reason a closed 94 | issue seems relevant once again, reopening is great and better than creating a 95 | duplicate issue. 96 | 97 | ## Everything else 98 | 99 | When in doubt, find the other maintainers and ask. 100 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.1.0", 4 | "configurations": [ 5 | { 6 | "name": "Run Extension", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 10 | "outFiles": ["${workspaceFolder}/out/**/*.js"], 11 | "preLaunchTask": "${defaultBuildTask}" 12 | }, 13 | { 14 | "name": "Debug Server", 15 | "type": "node", 16 | "request": "launch", 17 | "cwd": "${workspaceFolder}", 18 | "program": "${workspaceFolder}/src/debugger.ts", 19 | "args": ["--server=4711"], 20 | "outFiles": ["${workspaceFolder}/out/**/*.js"] 21 | }, 22 | { 23 | "name": "Extension Tests", 24 | "type": "extensionHost", 25 | "request": "launch", 26 | "args": [ 27 | "--extensionDevelopmentPath=${workspaceFolder}", 28 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 29 | ], 30 | "outFiles": ["${workspaceFolder}/out/test/**/*.js"], 31 | "preLaunchTask": "${defaultBuildTask}" 32 | } 33 | ], 34 | "compounds": [ 35 | { 36 | "name": "Extension + Debug Server", 37 | "configurations": ["Run Extension", "Debug Server"] 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off" 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | src/** 4 | .gitignore 5 | .yarnrc 6 | vsc-extension-quickstart.md 7 | **/tsconfig.json 8 | **/.eslintrc.json 9 | **/*.map 10 | **/*.ts 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | See the full list of recent releases and features added on the 4 | [Github releases page](https://github.com/PranayAgarwal/vscode-hack/releases). 5 | 6 | ## v2.20.0 - 2024-04-24 7 | 8 | - Update comment syntax (from `#` to `//`) in snippets. Thanks [Glitched](https://github.com/Glitched)! 9 | 10 | ## v2.19.0 - 2024-03-05 11 | 12 | - New status bar indicator for LSP -> hh_server connectivity issues. 13 | 14 | ## v2.18.0 - 2024-02-29 15 | 16 | - New autocomplete helpers for JSDoc comment blocks (`/** ... */`). 17 | - Removed `onDebug` as an activation event to prevent unnecessary extension activations. 18 | 19 | ## v2.17.1 - 2023-07-10 20 | 21 | - Automated and manual security fixes for dependencies. 22 | - Minor project housekeeping: 23 | - Updated some package paths (`vscode-debugadapter`, `vscode-debugprotocol` and `vsce` all moved under the `@vscode` npm org). 24 | - Added some required OSS notices to the repo. 25 | 26 | ## v2.16.0 - 2022-10-03 27 | 28 | - Syntax highlighting fixes. Thanks [Wilfred](https://github.com/Wilfred) & [panopticoncentral](https://github.com/panopticoncentral)! 29 | 30 | ## v2.15.0 - 2022-09-27 31 | 32 | - Syntax highlighting for await and concurrent. Thanks [Wilfred](https://github.com/Wilfred)! 33 | - Syntax highlighting for modules. Thanks [panopticoncentral](https://github.com/panopticoncentral)! 34 | 35 | ## v2.14.0 - 2022-08-25 36 | 37 | - Lots of syntax highlighting fixes. Thanks [Wilfred](https://github.com/Wilfred)! 38 | - Fix file path mappings on Windows. Thanks [skoro](https://github.com/skoro)! 39 | 40 | ## v2.13.0 - 2022-03-09 41 | 42 | - Removed custom workspace trust mechanism, since VS Code now has it built-in (thanks 43 | [@fredemmott](https://github.com/fredemmott))! 44 | - Automated publishing of extension package as a release asset. 45 | 46 | ## v2.12.0 - 2021-10-28 47 | 48 | - Reverted expression-trees syntax highlighting rule due to performance issues. 49 | - Removed error suppression code action. 50 | 51 | ## v2.11.0 - 2021-09-30 52 | 53 | - Syntax highlighting improvements. Thanks 54 | [Wilfred](https://github.com/Wilfred)! 55 | - Hack error suppression action in LSP mode. Thanks 56 | [icechen1](https://github.com/icechen1)! 57 | - `#region` folding support. Thanks [turadg](https://github.com/turadg)! 58 | 59 | ## v2.10.0 - 2020-08-18 60 | 61 | - Auto-start Hack typechecker on workspace load. Thanks 62 | [antoniodejesusochoasolano](https://github.com/antoniodejesusochoasolano)! 63 | - Syntax highlighting improvements 64 | 65 | ## v2.9.7 - 2020-08-18 66 | 67 | - Syntax highlighting fix (adding missed `Pair` literal). 68 | 69 | ## v2.9.6 - 2020-08-15 70 | 71 | - Syntax highlighting improvements. Thanks 72 | [Wilfred](https://github.com/Wilfred)! 73 | 74 | ## v2.9.5 - 2020-05-19 75 | 76 | - Updated snippets 77 | - Remove build badge from README 78 | 79 | ## v2.9.4 - 2020-03-02 80 | 81 | - Fix for "Invalid variable attributes" error in debugger panel when stopped on 82 | a breakpoint. 83 | 84 | ## v2.9.3 - 2020-01-30 85 | 86 | - Syntax highlighting fixes 87 | - Remove deprecated PHP snippets 88 | 89 | ## v2.9.2 - 2019-12-17 90 | 91 | - Fix `NOTICE.md` file, which was accidentally left blank in the last release. 92 | 93 | ## v2.9.1 - 2019-12-16 94 | 95 | - Better error messages when `hh_client` connection fails and extension is 96 | running in remote mode (using either SSH or Docker). Thanks 97 | [icechen1](https://github.com/icechen1)! 98 | 99 | ## v2.9.0 - 2019-11-22 100 | 101 | - New `launchUrl` option in debugger attach config automatically invokes a web 102 | request once the debugger is attached, and detaches when the request is 103 | complete. 104 | 105 | ## v2.8.1 - 2019-11-10 106 | 107 | - Syntax highlighting improvements. Thanks 108 | [scotchval](https://github.com/scotchval)! 109 | 110 | ## v2.8.0 - 2019-10-30 111 | 112 | - If the IDE session is connected to a remote typechecker of type `docker`, 113 | scripts run via the launch debug target now automatically start in the Docker 114 | container instead of the host machine. Stopping on breakpoints isn’t yet 115 | supported in this mode. 116 | 117 | ## v2.7.1 - 2019-10-28 118 | 119 | - Syntax highlighting fixes. Thanks [lildude](https://github.com/lildude)! 120 | 121 | ## v2.7.0 - 2019-10-17 122 | 123 | - Lots of syntax highlighting updates. Thanks 124 | [tspence](https://github.com/tspence)! 125 | 126 | ## v2.6.0 - 2019-09-16 127 | 128 | - Config values now support the `${workspaceFolder}` variable. 129 | 130 | ## v2.5.1 - 2019-07-26 131 | 132 | - Fix — in later HHVM versions the LSP doesn't send over connection status, so 133 | hide the status bar indicator for those cases. 134 | 135 | ## v2.5.0 - 2019-07-25 136 | 137 | - Added a connection indicator in the editor status bar which shows HHVM version 138 | & LSP status/errors. This should make it easier to debug problems in IDE 139 | functionality related to hh_server crashes or restarts. 140 | 141 | ## v2.4.1 - 2019-07-23 142 | 143 | - Bug fixes in coverage checker 144 | - Syntax highlighting updates 145 | 146 | ## v2.4.0 - 2019-05-15 147 | 148 | - Add support for Unix domain sockets for debugger "attach" target. 149 | 150 | ## v2.3.0 - 2019-05-06 151 | 152 | - Show modal asking users to reload the workspace on changes to `hack.remote.*` 153 | configuration settings. 154 | 155 | ## v2.2.0 - 2019-04-22 156 | 157 | - **Syntax Highlighting Updates** — Support for `__dispose`, `__disposeAsync`, 158 | `newtype` and `async`. 159 | 160 | ## v2.1.0 - 2019-03-08 161 | 162 | - **Syntax Highlighting Updates** — Editor will now recognize `arraykey`, 163 | `nonnull`, `dict`, `vec` and `keyset` as valid storage types. 164 | 165 | ## v2.0.0 - 2019-03-06 166 | 167 | - **Remote language server connection support** — You can now connect to an 168 | external development environment for Hack typechecking, linting and all other 169 | intellisense features. Current supported methods are SSH and Docker. See the 170 | **Remote Development** section in README.md for more details. 171 | - This version may cause breaking changes to your existing setup if you were 172 | already using Docker via a custom `hack.clientPath` executable. 173 | - The `hack.workspaceRootPath` config has been renamed to 174 | `hack.remote.workspacePath`. 175 | - Running the extension with LSP mode disabled is now unsupported. It will be 176 | fully removed in a future version of the extension. 177 | 178 | ## v1.2.1 - 2019-02-19 179 | 180 | - Fixed [#40](https://github.com/slackhq/vscode-hack/issues/40) — Syntax 181 | highlighting breaks for `.hack` files that contain ` 79 | 80 | Permission is hereby granted, free of charge, to any person obtaining a copy 81 | of this software and associated documentation files (the "Software"), to deal 82 | in the Software without restriction, including without limitation the rights 83 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 84 | copies of the Software, and to permit persons to whom the Software is 85 | furnished to do so, subject to the following conditions: 86 | 87 | The above copyright notice and this permission notice shall be included in all 88 | copies or substantial portions of the Software. 89 | 90 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 91 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 92 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 93 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 94 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 95 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 96 | SOFTWARE. 97 | ``` 98 | 99 | **[lru-cache@6.0.0](https://github.com/isaacs/node-lru-cache)** 100 | ``` 101 | The ISC License 102 | 103 | Copyright (c) Isaac Z. Schlueter and Contributors 104 | 105 | Permission to use, copy, modify, and/or distribute this software for any 106 | purpose with or without fee is hereby granted, provided that the above 107 | copyright notice and this permission notice appear in all copies. 108 | 109 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 110 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 111 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 112 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 113 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 114 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 115 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 116 | ``` 117 | 118 | **[minimatch@5.1.6](https://github.com/isaacs/minimatch)** 119 | ``` 120 | The ISC License 121 | 122 | Copyright (c) 2011-2023 Isaac Z. Schlueter and Contributors 123 | 124 | Permission to use, copy, modify, and/or distribute this software for any 125 | purpose with or without fee is hereby granted, provided that the above 126 | copyright notice and this permission notice appear in all copies. 127 | 128 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 129 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 130 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 131 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 132 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 133 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 134 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 135 | ``` 136 | 137 | **[semver@7.6.0](https://github.com/npm/node-semver)** 138 | ``` 139 | The ISC License 140 | 141 | Copyright (c) Isaac Z. Schlueter and Contributors 142 | 143 | Permission to use, copy, modify, and/or distribute this software for any 144 | purpose with or without fee is hereby granted, provided that the above 145 | copyright notice and this permission notice appear in all copies. 146 | 147 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 148 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 149 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 150 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 151 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 152 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 153 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 154 | ``` 155 | 156 | **[vscode-jsonrpc@8.2.0](https://github.com/Microsoft/vscode-languageserver-node)** 157 | ``` 158 | Copyright (c) Microsoft Corporation 159 | 160 | All rights reserved. 161 | 162 | MIT License 163 | 164 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 165 | 166 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 167 | 168 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 169 | ``` 170 | 171 | **[vscode-languageclient@9.0.1](https://github.com/Microsoft/vscode-languageserver-node)** 172 | ``` 173 | Copyright (c) Microsoft Corporation 174 | 175 | All rights reserved. 176 | 177 | MIT License 178 | 179 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 180 | 181 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 182 | 183 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 184 | ``` 185 | 186 | **[vscode-languageserver-protocol@3.17.5](https://github.com/Microsoft/vscode-languageserver-node)** 187 | ``` 188 | Copyright (c) Microsoft Corporation 189 | 190 | All rights reserved. 191 | 192 | MIT License 193 | 194 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 195 | 196 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 197 | 198 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 199 | ``` 200 | 201 | **[vscode-languageserver-types@3.17.5](https://github.com/Microsoft/vscode-languageserver-node)** 202 | ``` 203 | Copyright (c) Microsoft Corporation 204 | 205 | All rights reserved. 206 | 207 | MIT License 208 | 209 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 210 | 211 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 212 | 213 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 214 | ``` 215 | 216 | **[yallist@4.0.0](https://github.com/isaacs/yallist)** 217 | ``` 218 | The ISC License 219 | 220 | Copyright (c) Isaac Z. Schlueter and Contributors 221 | 222 | Permission to use, copy, modify, and/or distribute this software for any 223 | purpose with or without fee is hereby granted, provided that the above 224 | copyright notice and this permission notice appear in all copies. 225 | 226 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 227 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 228 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 229 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 230 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 231 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR 232 | IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 233 | ``` 234 | 235 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hack for Visual Studio Code 2 | 3 | This extension adds rich Hack language & HHVM support to Visual Studio Code. Visit [http://hacklang.org](http://hacklang.org) to get started with Hack. 4 | 5 | It is published in the Visual Studio Marketplace [here](https://marketplace.visualstudio.com/items?itemName=pranayagarwal.vscode-hack). To install, search for "Hack" in the VS Code extensions tab or run the following command (⌘+P): `ext install vscode-hack`. 6 | 7 | ## Latest releases 8 | 9 | ## v2.9.0 10 | 11 | - New `launchUrl` option in debugger attach config automatically invokes a web request once the debugger is attached, and detaches when the request is complete. 12 | 13 | ## v2.8.0 14 | 15 | - If the IDE session is connected to a remote typechecker of type `docker`, scripts run via the launch debug target now automatically start in the Docker container instead of the host machine. Stopping on breakpoints isn’t yet supported in this mode. 16 | 17 | ## v2.7.0 18 | 19 | - Lots of syntax highlighting updates. Thanks [tspence](https://github.com/tspence)! 20 | 21 | See the full list of releases and features added on the [Github releases page](https://github.com/slackhq/vscode-hack/releases) as well as the project [changelog](https://github.com/slackhq/vscode-hack/blob/master/CHANGELOG.md). 22 | 23 | ## Features 24 | 25 | - Type Checking 26 | - Autocomplete 27 | - Hover Hints 28 | - Document Symbol Outline 29 | - Workspace Symbol Search 30 | - Document Formatting 31 | - Go To/Peek Definition 32 | - Find All References 33 | - Hack Coverage Check 34 | - Linting and Autofixing 35 | - [Local and Remote Debugging](https://github.com/slackhq/vscode-hack/blob/master/docs/debugging.md) 36 | 37 | ![Hack for Visual Studio Code](https://cloud.githubusercontent.com/assets/341507/19377806/d7838da0-919d-11e6-9873-f5a6aa48aea4.gif) 38 | 39 | ## Requirements 40 | 41 | This extension is supported on Linux and Mac OS X 10.10 onwards ([see HHVM compatibility](https://docs.hhvm.com/hhvm/installation/introduction)). The latest versions of Hack typechecking tools (`hh_client` and `hh_server`) are required on the local machine or via a remote connection. The workspace should have a `.hhconfig` file at its root. 42 | 43 | ## Configuration 44 | 45 | This extension adds the following Visual Studio Code settings. These can be set in user preferences (⌘+,) or workspace settings (`.vscode/settings.json`). 46 | 47 | - `hack.clientPath`: Absolute path to the hh_client executable. This can be left empty if hh_client is already in your environment \$PATH. 48 | - `hack.enableCoverageCheck`: Enable calculation of Hack type coverage percentage for every file and display in status bar (default: `true`). 49 | - `hack.useLanguageServer`: Start hh_client in Language Server mode. Only works for HHVM version 3.23 and above (default: `true`). 50 | - `hack.useHhast`: Enable linting (needs [HHAST](https://github.com/hhvm/hhast) library set up and configured in project) (default: `true`). 51 | - `hack.hhastPath`: Use an alternate `hhast-lint` path. Can be abolute or relative to workspace root (default: `vendor/bin/hhast-lint`). 52 | - `hack.hhastArgs`: Optional list of arguments passed to hhast-lint executable. 53 | - `hack.hhastLintMode`: Whether to lint the entire project (`whole-project`) or just the open files (`open-files`). 54 | 55 | ### Remote Development 56 | 57 | The extension supports connecting to an external HHVM development environment for local typechecking, linting and all other intellisense features. The current supported connection methods are SSH into a remote host or exec in a local Docker container. 58 | 59 | To enable this, set the following config values: 60 | 61 | - `hack.remote.enabled`: Run the Hack language tools on an external host (deafult: `false`). 62 | - `hack.remote.type`: The remote connection method (`ssh` or `docker`). 63 | - `hack.remote.workspacePath`: Absolute location of workspace root in the remote file system. If empty, this defaults to the local workspace path. 64 | 65 | **For SSH:** 66 | 67 | - `hack.remote.ssh.host`: Address for the remote development server to connect to (in the format `[user@]hostname`). 68 | - `hack.remote.ssh.flags`: Additional command line options to pass when establishing the SSH connection (_Optional_). 69 | 70 | Make sure to test SSH connectivity and credentials beforehand. You should also ensure that the source stays in sync between the local and remote machines (the extension doesn't currently handle this). 71 | 72 | ```bash 73 | $ ssh user@my-remote-host.com "cd /mnt/project && hh_client" 74 | No errors! 75 | ``` 76 | 77 | **For Docker:** 78 | 79 | - `hack.remote.docker.containerName`: Name of the local Docker container to run the language tools in. 80 | 81 | Make sure the container is already running on your local machine, and has the required HHVM setup. You can pull an [official HHVM image](https://hub.docker.com/r/hhvm/hhvm/) from Docker hub and even run multiple versions simultaneously. 82 | 83 | ```bash 84 | $ docker run -d -t --name my-hhvm -v /home/user/repos/project:/mnt/project hhvm/hhvm:latest 85 | $ docker exec --workdir /mnt/project my-hhvm hh_client 86 | No errors! 87 | ``` 88 | 89 | ## Issues 90 | 91 | Please file all bugs, issues, feature requests etc. at the [GitHub issues page](https://github.com/slackhq/vscode-hack/issues). 92 | 93 | ## Contributing 94 | 95 | There are lots of ways to help! You can file new bugs and feature requests, or fix a pending one. To contribute to the source code, fork the repository on GitHub and create a pull request. Please read our [Contributors Guide](CONTRIBUTING.md) and check out the [VS Code extension development guide](https://code.visualstudio.com/docs/extensions/overview) to get started. 96 | 97 | ## License 98 | 99 | The source code for this extension is hosted at [https://github.com/slackhq/vscode-hack](https://github.com/slackhq/vscode-hack) and is available under the [MIT license](LICENSE.md). 100 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Security 2 | 3 | Please report any security issue to [security@salesforce.com](mailto:security@salesforce.com) 4 | as soon as it is discovered. This library limits its runtime dependencies in 5 | order to reduce the total cost of ownership as much as can be, but all consumers 6 | should remain vigilant and have their security stakeholders review all third-party 7 | products (3PP) like this one and their dependencies. 8 | -------------------------------------------------------------------------------- /build/notice-file-generate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This script generates a NOTICE.md file at project root containing licenses for all 3 | * third-party dependencies included in the published extension package. 4 | */ 5 | 6 | console.log("Generating NOTICE.md file..."); 7 | 8 | var checker = require("license-checker"); 9 | var fs = require("fs"); 10 | var path = require("path"); 11 | 12 | checker.init( 13 | { 14 | start: `${__dirname}/..`, 15 | production: true, 16 | customFormat: { 17 | licenseText: "", 18 | }, 19 | }, 20 | function (err, packages) { 21 | if (err) { 22 | console.error(`Failed to generate NOTICE.md file: ${err.stack}`); 23 | process.exit(1); 24 | } else { 25 | var stream = fs.createWriteStream( 26 | path.resolve(__dirname, "..", "NOTICE.md"), 27 | ); 28 | stream.write(`# Third-party notices\n\n 29 | This document includes licensing information relating to free, open-source, and public-source software (together, the “SOFTWARE”) included with or used while developing Slack’s \`vscode-hack\` software. The terms of the applicable free, open-source, and public-source licenses (each an “OSS LICENSE”) govern Slack’s distribution and your use of the SOFTWARE. Slack and the third-party authors, licensors, and distributors of the SOFTWARE disclaim all warranties and liability arising from all use and distribution of the SOFTWARE. To the extent the OSS is provided under an agreement with Slack that differs from the applicable OSS LICENSE, those terms are offered by Slack alone.\n\n 30 | Slack has reproduced below copyright and other licensing notices appearing within the SOFTWARE. While Slack seeks to provide complete and accurate copyright and licensing information for each SOFTWARE package, Slack does not represent or warrant that the following information is complete, correct, or error-free. SOFTWARE recipients are encouraged to (a) investigate the identified SOFTWARE packages to confirm the accuracy of the licensing information provided herein and (b) notify Slack of any inaccuracies or errors found in this document so that Slack may update this document accordingly.\n\n 31 | Certain OSS LICENSES (such as the GNU General Public Licenses, GNU Library/Lesser General Public Licenses, Affero General Public Licenses, Mozilla Public Licenses, Common Development and Distribution Licenses, Common Public License, and Eclipse Public License) require that the source code corresponding to distributed OSS binaries be made available to recipients or other requestors under the terms of the same OSS LICENSE. Recipients or requestors who would like to receive a copy of such corresponding source code should submit a request to Slack by post at:\n\n 32 | Slack 33 | Attn: Open Source Requests 34 | 500 Howard St. 35 | San Francisco, CA 94105\n\n---\n`); 36 | for (var package in packages) { 37 | if (package.startsWith("vscode-hack@")) { 38 | // Don't need to include a separate notice for our own package 39 | continue; 40 | } 41 | stream.write(`**[${package}](${packages[package].repository})**\n`); 42 | stream.write(`\`\`\`\n${packages[package].licenseText}\n\`\`\`\n\n`); 43 | } 44 | stream.end(); 45 | console.log(`NOTICE.md successfully written at ${stream.path}\n`); 46 | } 47 | }, 48 | ); 49 | -------------------------------------------------------------------------------- /docs/debugging.md: -------------------------------------------------------------------------------- 1 | # Using the HHVM Debugger 2 | 3 | HHVM versions 3.25 and later come with a built-in debugging extension that can be enabled with a runtime flag. You can use the VS Code debug interface to launch and debug PHP/Hack scripts or attach to an existing HHVM server process on a local or remote machine. 4 | 5 | ## For local script execution 6 | 7 | Add a new HHVM `launch` config to `.vscode/launch.json`. The default template should be good enough, but you can change the values if needed: 8 | 9 | `script`: The PHP/Hack script to launch. Use `${file}` to run the currently open file in the editor, or set to any other static file path. 10 | `hhvmPath`: [Optional] Absolute path to the HHVM executable (default `hhvm`) 11 | `hhvmArgs`: [Optional] Extra arguments to pass to HHVM when launching the script, if needed 12 | `cwd`: [Optional] Working directory for the HHVM process 13 | 14 | Debug -> Start Debugging (F5) with this configuration selected will launch the currently open PHP/Hack script (or the custom configured file) in a new HHVM process and pipe input/output to the editor OUTPUT tab. 15 | 16 | You will need to set breakpoints before script execution to be able to hit them. 17 | 18 | ## For remote server debugging 19 | 20 | Start your HHVM server with the following additional configuration strings in server.ini or CLI args: 21 | 22 | `hhvm.debugger.vs_debug_enable=1` to enable the debugging extension 23 | `hhvm.debugger.vs_debug_listen_port=` to optionally change the port the debugger listens on (default: `8999`) 24 | `hhvm.debugger.vs_debug_domain_socket_path=` to optionally expose the debugger interface over a unix socket rather than a TCP port 25 | 26 | E.g. `hhvm -m server -p 8080 -d hhvm.debugger.vs_debug_enable=1 -d hhvm.debugger.vs_debug_listen_port=1234` 27 | 28 | You can also use the `--mode vsdebug`, `--vsDebugPort` and `--vsDebugDomainSocketPath` command line arguments for the same purpose. 29 | 30 | Add a new HHVM `attach` config to `.vscode/launch.json` and configure as needed: 31 | 32 | `host`: [Optional] The remote HHVM host (default: `localhost`) 33 | `port`: [Optional] The server debugging port, if changed in HHVM config (default: `8999`) 34 | `socket`: [Optional] Path to a Unix domain socket path. If specified, the debugger will attach to this socket rather than a TCP port. 35 | 36 | If the site root on the server is different from your local workspace, set the following to automatically map them: 37 | 38 | `remoteSiteRoot`: [Optional] Absolute path to site root on the HHVM server 39 | `localWorkspaceRoot`: [Optional] Absolute path to local workspace root. Set to `${workspaceFolder}` to use the current VS Code workspace. 40 | 41 | ### SSH tunneling 42 | 43 | The HHVM debugger port is only exposed on the localhost (loopback) interface, so unless the server is running on the local machine you will likely need to foward a local port to the remote host via a SSH tunnel: 44 | 45 | `ssh -nNT -L 8999:localhost:8999 user@remote.machine.com` 46 | 47 | You can now connect to localhost:8999 as normal. 48 | 49 | ### Docker containers 50 | 51 | For the same reason, Docker port forwarding won't work for the debugger port. You need to either use your host network driver for the container (`docker run` with `--network host`) or use SSH tunneling or other solutions like [socat](http://www.dest-unreach.org/socat/) instead. 52 | 53 | E.g. publish port 8998 to the Docker host and in your container install and run: 54 | 55 | `socat tcp-listen:8998,reuseaddr,fork tcp:localhost:8999` 56 | 57 | This will foward connections to exposed port 8998 to port 8999 in your container. 58 | 59 | ### Common server debugging setups 60 | 61 | ![Common debugging setups](images/debugger-setups.png) 62 | 63 | 1. Single PHP/Hack script launched by VS Code 64 | 2. Everything running locally on the same network 65 | 3. Connected to debugging port on a remote server through a SSH tunnel 66 | 4. Running HHVM in a Docker container and forwarding debugger port via [socat](http://www.dest-unreach.org/socat/doc/socat.html) 67 | -------------------------------------------------------------------------------- /docs/images/debugger-setups.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slackhq/vscode-hack/2da10a05b70c9aae672f25bc218fe92d44522408/docs/images/debugger-setups.png -------------------------------------------------------------------------------- /hack.configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "comments": { 3 | "lineComment": "//", 4 | "blockComment": ["/*", "*/"] 5 | }, 6 | "brackets": [ 7 | ["{", "}"], 8 | ["[", "]"], 9 | ["(", ")"] 10 | ], 11 | "autoClosingPairs": [ 12 | ["{", "}"], 13 | ["[", "]"], 14 | ["(", ")"], 15 | ["\"", "\""], 16 | ["'", "'"], 17 | { 18 | "open": "/**", 19 | "close": " */", 20 | "notIn": ["string"] 21 | } 22 | ], 23 | "surroundingPairs": [ 24 | ["{", "}"], 25 | ["[", "]"], 26 | ["(", ")"], 27 | ["\"", "\""], 28 | ["'", "'"] 29 | ], 30 | "folding": { 31 | "markers": { 32 | "start": "^\\s*//\\s*#?region\\b", 33 | "end": "^\\s*//\\s*#?endregion\\b" 34 | } 35 | }, 36 | "onEnterRules": [ 37 | { 38 | "beforeText": { 39 | "pattern": "^\\s*/\\*\\*(?!/)([^\\*]|\\*(?!/))*$" 40 | }, 41 | "afterText": { 42 | "pattern": "^\\s*\\*/$" 43 | }, 44 | "action": { 45 | "indent": "indentOutdent", 46 | "appendText": " * " 47 | } 48 | }, 49 | { 50 | "beforeText": { 51 | "pattern": "^\\s*/\\*\\*(?!/)([^\\*]|\\*(?!/))*$" 52 | }, 53 | "action": { 54 | "indent": "none", 55 | "appendText": " * " 56 | } 57 | }, 58 | { 59 | "beforeText": { 60 | "pattern": "^(\\t|[ ])*[ ]\\*([ ]([^\\*]|\\*(?!/))*)?$" 61 | }, 62 | "previousLineText": { 63 | "pattern": "(?=^(\\s*(/\\*\\*|\\*)).*)(?=(?!(\\s*\\*/)))" 64 | }, 65 | "action": { 66 | "indent": "none", 67 | "appendText": "* " 68 | } 69 | }, 70 | { 71 | "beforeText": { 72 | "pattern": "^(\\t|[ ])*[ ]\\*/\\s*$" 73 | }, 74 | "action": { 75 | "indent": "none", 76 | "removeText": 1 77 | } 78 | }, 79 | { 80 | "beforeText": { 81 | "pattern": "^(\\t|[ ])*[ ]\\*[^/]*\\*/\\s*$" 82 | }, 83 | "action": { 84 | "indent": "none", 85 | "removeText": 1 86 | } 87 | }, 88 | { 89 | "beforeText": { 90 | "pattern": "^\\s*(\\bcase\\s.+:|\\bdefault:)$" 91 | }, 92 | "afterText": { 93 | "pattern": "^(?!\\s*(\\bcase\\b|\\bdefault\\b))" 94 | }, 95 | "action": { 96 | "indent": "indent" 97 | } 98 | } 99 | ] 100 | } 101 | -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/slackhq/vscode-hack/2da10a05b70c9aae672f25bc218fe92d44522408/images/logo.png -------------------------------------------------------------------------------- /images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-hack", 3 | "version": "2.20.0", 4 | "publisher": "pranayagarwal", 5 | "engines": { 6 | "vscode": "^1.74.0" 7 | }, 8 | "license": "MIT", 9 | "displayName": "Hack", 10 | "description": "Hack language & HHVM debugger support for Visual Studio Code", 11 | "icon": "images/logo.png", 12 | "categories": [ 13 | "Programming Languages", 14 | "Debuggers", 15 | "Linters", 16 | "Snippets", 17 | "Formatters", 18 | "Other" 19 | ], 20 | "keywords": [ 21 | "hack", 22 | "hacklang", 23 | "hhvm", 24 | "php" 25 | ], 26 | "galleryBanner": { 27 | "color": "#5d5d5d", 28 | "theme": "dark" 29 | }, 30 | "author": { 31 | "name": "Pranay Agarwal" 32 | }, 33 | "repository": { 34 | "type": "git", 35 | "url": "https://github.com/slackhq/vscode-hack.git" 36 | }, 37 | "bugs": { 38 | "url": "https://github.com/slackhq/vscode-hack/issues" 39 | }, 40 | "main": "./out/main", 41 | "contributes": { 42 | "languages": [ 43 | { 44 | "id": "hack", 45 | "aliases": [ 46 | "Hack", 47 | "hacklang" 48 | ], 49 | "configuration": "./hack.configuration.json", 50 | "extensions": [ 51 | ".php", 52 | ".hh", 53 | ".hack" 54 | ], 55 | "firstLine": "^<\\?hh\\b.*|#!.*hhvm.*$" 56 | }, 57 | { 58 | "id": "ini", 59 | "aliases": [ 60 | "Hack Configuration", 61 | "hack", 62 | "hacklang" 63 | ], 64 | "filenames": [ 65 | ".hhconfig" 66 | ] 67 | } 68 | ], 69 | "grammars": [ 70 | { 71 | "language": "hack", 72 | "scopeName": "source.hack", 73 | "path": "./syntaxes/hack.json" 74 | }, 75 | { 76 | "scopeName": "markdown.hack.codeblock", 77 | "path": "./syntaxes/codeblock.json", 78 | "injectTo": [ 79 | "text.html.markdown" 80 | ], 81 | "embeddedLanguages": { 82 | "meta.embedded.block.hack": "hack" 83 | } 84 | } 85 | ], 86 | "snippets": [ 87 | { 88 | "language": "hack", 89 | "path": "./snippets/hack.json" 90 | } 91 | ], 92 | "breakpoints": [ 93 | { 94 | "language": "hack" 95 | } 96 | ], 97 | "debuggers": [ 98 | { 99 | "type": "hhvm", 100 | "label": "HHVM", 101 | "program": "./out/debugger", 102 | "runtime": "node", 103 | "languages": [ 104 | "hack" 105 | ], 106 | "configurationAttributes": { 107 | "attach": { 108 | "properties": { 109 | "host": { 110 | "type": "string", 111 | "description": "Host name/IP address of HHVM server (default: localhost)", 112 | "default": "localhost" 113 | }, 114 | "port": { 115 | "type": "number", 116 | "description": "Debug port to attach to (default: 8999)", 117 | "default": 8999 118 | }, 119 | "socket": { 120 | "type": "string", 121 | "description": "Path to the Unix domain socket to attach to" 122 | }, 123 | "remoteSiteRoot": { 124 | "type": "string", 125 | "description": "Absolute path to workspace root on the remote server, to map to local workspace", 126 | "default": "${workspaceFolder}" 127 | }, 128 | "localWorkspaceRoot": { 129 | "type": "string", 130 | "description": "Absolute path to local workspace root, to map to remote server", 131 | "default": "${workspaceFolder}" 132 | }, 133 | "launchUrl": { 134 | "type": "string", 135 | "description": "Make an HTTP GET request to this URL once the debugger is attached, and automatically detach when the request is complete" 136 | } 137 | } 138 | }, 139 | "launch": { 140 | "properties": { 141 | "hhvmPath": { 142 | "type": "string", 143 | "description": "Absolute path to HHVM executable for launching scripts (default: hhvm)", 144 | "default": "hhvm" 145 | }, 146 | "hhvmArgs": { 147 | "type": "array", 148 | "description": "Extra arguments to pass to the HHVM command when launching a script, if any" 149 | }, 150 | "script": { 151 | "type": "string", 152 | "description": "The PHP/Hack script to launch", 153 | "default": "${file}" 154 | }, 155 | "cwd": { 156 | "type": "string", 157 | "description": "Working directory for the launched HHVM process", 158 | "default": "${workspaceFolder}" 159 | } 160 | } 161 | } 162 | }, 163 | "configurationSnippets": [ 164 | { 165 | "label": "HHVM: Attach to Server", 166 | "description": "Attach to an HHVM server", 167 | "body": { 168 | "name": "HHVM: Attach to Server", 169 | "type": "hhvm", 170 | "request": "attach", 171 | "host": "localhost", 172 | "port": 8999, 173 | "remoteSiteRoot": "^\"\\${workspaceFolder}\"", 174 | "localWorkspaceRoot": "^\"\\${workspaceFolder}\"" 175 | } 176 | }, 177 | { 178 | "label": "HHVM: Run Script", 179 | "description": "Run the current script", 180 | "body": { 181 | "name": "HHVM: Run Script", 182 | "type": "hhvm", 183 | "request": "launch", 184 | "script": "^\"\\${file}\"" 185 | } 186 | } 187 | ] 188 | } 189 | ], 190 | "configuration": { 191 | "type": "object", 192 | "title": "Hack configuration", 193 | "properties": { 194 | "hack.clientPath": { 195 | "type": "string", 196 | "default": "hh_client", 197 | "description": "Absolute path to the hh_client executable. This can be left empty if hh_client is already in your environment $PATH." 198 | }, 199 | "hack.workspaceRootPath": { 200 | "type": "string", 201 | "default": null, 202 | "description": "Absolute path to the workspace root directory. This will be the VS Code workspace root by default, but can be changed if the project is in a subdirectory or mounted in a Docker container.", 203 | "deprecationMessage": "Use hack.remote.workspacePath instead" 204 | }, 205 | "hack.enableCoverageCheck": { 206 | "type": "boolean", 207 | "default": false, 208 | "description": "Enable calculation of Hack type coverage percentage for every file and display in status bar." 209 | }, 210 | "hack.useLanguageServer": { 211 | "type": "boolean", 212 | "default": true, 213 | "description": "Start hh_client in Language Server mode. Only works for HHVM version 3.23 and above." 214 | }, 215 | "hack.useHhast": { 216 | "type": "boolean", 217 | "default": true, 218 | "description": "Enable linting (needs HHAST library set up and configured in project)", 219 | "markdownDescription": "Enable linting (needs [HHAST](https://github.com/hhvm/hhast) library set up and configured in project)" 220 | }, 221 | "hack.hhastPath": { 222 | "type": "string", 223 | "default": "vendor/bin/hhast-lint", 224 | "description": "Use an alternate hhast-lint path. Can be abolute or relative to workspace root.", 225 | "markdownDescription": "Use an alternate `hhast-lint` path. Can be abolute or relative to workspace root." 226 | }, 227 | "hack.hhastArgs": { 228 | "type": "array", 229 | "items": { 230 | "type": "string" 231 | }, 232 | "default": [], 233 | "description": "Optional list of arguments passed to hhast-lint executable" 234 | }, 235 | "hack.hhastLintMode": { 236 | "type": "string", 237 | "enum": [ 238 | "whole-project", 239 | "open-files" 240 | ], 241 | "enumDescriptions": [ 242 | "Lint the entire project and show all errors", 243 | "Only lint the currently open files" 244 | ], 245 | "default": null, 246 | "description": "Whether to lint the entire project or just the open files" 247 | }, 248 | "hack.remote.enabled": { 249 | "type": "boolean", 250 | "default": false, 251 | "description": "Run the Hack language tools on an external host" 252 | }, 253 | "hack.remote.type": { 254 | "type": "string", 255 | "enum": [ 256 | "ssh", 257 | "docker" 258 | ], 259 | "enumDescriptions": [ 260 | "Run typechecker on a remote server via SSH", 261 | "Run typechecker in a Docker container" 262 | ], 263 | "description": "The remote connection method" 264 | }, 265 | "hack.remote.workspacePath": { 266 | "type": "string", 267 | "description": "Absolute location of workspace root in the remote file system" 268 | }, 269 | "hack.remote.ssh.host": { 270 | "type": "string", 271 | "description": "Address for the remote development server to connect to (in the format `[user@]hostname`)" 272 | }, 273 | "hack.remote.ssh.flags": { 274 | "type": "array", 275 | "description": "Additional command line options to pass when establishing the SSH connection" 276 | }, 277 | "hack.remote.docker.containerName": { 278 | "type": "string", 279 | "description": "Name of the local Docker container to run the language tools in" 280 | }, 281 | "hack.trace.server": { 282 | "type": "string", 283 | "scope": "window", 284 | "enum": [ 285 | "off", 286 | "messages", 287 | "verbose" 288 | ], 289 | "default": "off", 290 | "description": "Traces the communication between VS Code and the Hack & HHAST language servers" 291 | } 292 | } 293 | }, 294 | "commands": [ 295 | { 296 | "command": "hack.toggleCoverageHighlight", 297 | "title": "Hack: Toggle Coverage Highlight" 298 | } 299 | ] 300 | }, 301 | "activationEvents": [ 302 | "workspaceContains:.hhconfig" 303 | ], 304 | "scripts": { 305 | "vscode:prepublish": "npm run compile", 306 | "compile": "tsc -p ./", 307 | "watch": "tsc -watch -p ./", 308 | "postinstall": "node ./build/notice-file-generate.js", 309 | "pretest": "npm run compile", 310 | "test": "node ./out/test/runTest.js", 311 | "package": "vsce package" 312 | }, 313 | "dependencies": { 314 | "@vscode/debugadapter": "^1.65.0", 315 | "@vscode/debugprotocol": "^1.65.0", 316 | "vscode-languageclient": "^9.0.1" 317 | }, 318 | "devDependencies": { 319 | "@types/glob": "^8.1.0", 320 | "@types/mocha": "^10.0.6", 321 | "@types/node": "^20.11.24", 322 | "@types/vscode": "^1.74.0", 323 | "@typescript-eslint/eslint-plugin": "^7.1.1", 324 | "@typescript-eslint/parser": "^7.1.1", 325 | "@vscode/test-electron": "^2.3.9", 326 | "@vscode/vsce": "^2.24.0", 327 | "eslint": "^8.57.0", 328 | "glob": "^7.2.3", 329 | "license-checker": "^25.0.1", 330 | "mocha": "^10.3.0", 331 | "typescript": "^5.3.3" 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /snippets/hack.json: -------------------------------------------------------------------------------- 1 | { 2 | ".source.hack": { 3 | "function __construct": { 4 | "prefix": "con", 5 | "body": "${1:public} function __construct($2) {\n\t${3:// code...;}$0\n}" 6 | }, 7 | "Class Variable": { 8 | "prefix": "doc_v", 9 | "body": "/**\n * ${4:undocumented class variable}\n */\n${1:private} ${2:type} $${3:name};$0" 10 | }, 11 | "Start Docblock": { 12 | "prefix": "/**", 13 | "body": "/**\n * $0\n */" 14 | }, 15 | "class …": { 16 | "prefix": "class", 17 | "body": "/**\n * $1\n */\nclass ${2:ClassName} ${3:extends ${4:AnotherClass}} {\n\t${5:public} function ${6:__construct}($7) {\n\t\t${0:// code...}\n\t}\n}\n" 18 | }, 19 | "trait …": { 20 | "prefix": "trait", 21 | "body": "/**\n * $1\n */\ntrait ${2:TraitName} {\n\tfunction ${3:functionName}($4): $5 {\n\t\t${6:// code...}\n\t}\n}\n" 22 | }, 23 | "const _ _ = _": { 24 | "prefix": "const", 25 | "body": "const ${1:type} ${2:name} = ${3:value};\n$0" 26 | }, 27 | "do … while …": { 28 | "prefix": "do", 29 | "body": "do {\n\t${0:// code...}\n} while (${1:\\$a <= 10});" 30 | }, 31 | "echo \"…\"": { 32 | "prefix": "echo", 33 | "body": "echo \"${1:string}\";$0" 34 | }, 35 | "else …": { 36 | "prefix": "else", 37 | "body": "else {\n\t${0:// code...}\n}" 38 | }, 39 | "elseif …": { 40 | "prefix": "elseif", 41 | "body": "else if (${1:condition}) {\n\t${0:// code...}\n}" 42 | }, 43 | "for …": { 44 | "prefix": "for", 45 | "body": "for ($${1:i} = ${2:0}; $${1:i} < $3; $${1:i}++) { \n\t${0:// code...}\n}" 46 | }, 47 | "foreach …": { 48 | "prefix": "foreach", 49 | "body": "foreach ($${1:variable} as $${2:key} ${3:=> $${4:value}}) {\n\t${0:// code...}\n}" 50 | }, 51 | "function …": { 52 | "prefix": "func", 53 | "body": "${1:public }function ${2:functionName}(${3:$${4:value}${5:=''}})\n{\n\t${0:// code...}\n}" 54 | }, 55 | "if … else …": { 56 | "prefix": "ifelse", 57 | "body": "if (${1:condition}) {\n\t${2:// code...}\n} else {\n\t${3:// code...}\n}\n$0" 58 | }, 59 | "if …": { 60 | "prefix": "if", 61 | "body": "if (${1:condition}) {\n\t${0:// code...}\n}" 62 | }, 63 | "$… = ( … ) ? … : …": { 64 | "prefix": "if?", 65 | "body": "$${1:retVal} = (${2:condition}) ? ${3:\\$a} : ${4:\\$b} ;" 66 | }, 67 | "… => …": { 68 | "prefix": "keyval", 69 | "body": "'$1' => $2${3:,} $0" 70 | }, 71 | "require_once …": { 72 | "prefix": "req1", 73 | "body": "require_once '${1:file}';\n$0" 74 | }, 75 | "return": { 76 | "prefix": "ret", 77 | "body": "return $1;$0" 78 | }, 79 | "return false": { 80 | "prefix": "ret0", 81 | "body": "return false;$0" 82 | }, 83 | "return true": { 84 | "prefix": "ret1", 85 | "body": "return true;$0" 86 | }, 87 | "switch …": { 88 | "prefix": "switch", 89 | "body": "switch (${1:variable}) {\n\tcase '${2:value}':\n\t\t${3:// code...}\n\t\tbreak;\n\t$0\n\tdefault:\n\t\t${4:// code...}\n\t\tbreak;\n}" 90 | }, 91 | "case …": { 92 | "prefix": "case", 93 | "body": "case '${1:variable}':\n\t${0:// code...}\n\tbreak;" 94 | }, 95 | "$this->…": { 96 | "prefix": "this", 97 | "body": "\\$this->$0" 98 | }, 99 | "echo $this->…": { 100 | "prefix": "ethis", 101 | "body": "echo \\$this->$1;\n$0" 102 | }, 103 | "Throw Exception": { 104 | "prefix": "throw", 105 | "body": "throw new $1Exception(${2:\"${3:Error Processing Request}\"}${4:, ${5:1}});\n$0" 106 | }, 107 | "while …": { 108 | "prefix": "while", 109 | "body": "while (${1:$$a <= 10}) {\n\t${0:// code...}\n}" 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /src/Config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Loads values from VS Code config. It is currently only read at extension launch, 3 | * but config change watchers can be added here if needed. 4 | */ 5 | 6 | import * as vscode from "vscode"; 7 | 8 | const hackConfig = vscode.workspace.getConfiguration("hack"); 9 | 10 | // tslint:disable-next-line:no-non-null-assertion 11 | export const localWorkspacePath = 12 | vscode.workspace.workspaceFolders![0].uri.fsPath; 13 | 14 | export let clientPath = hackConfig.get("clientPath") || "hh_client"; 15 | clientPath = clientPath.replace("${workspaceFolder}", localWorkspacePath); 16 | 17 | export const enableCoverageCheck = hackConfig.get( 18 | "enableCoverageCheck", 19 | true, 20 | ); 21 | export const useLanguageServer = hackConfig.get( 22 | "useLanguageServer", 23 | true, 24 | ); 25 | export const useHhast = hackConfig.get("useHhast", true); 26 | export const hhastLintMode: "whole-project" | "open-files" = hackConfig.get( 27 | "hhastLintMode", 28 | "whole-project", 29 | ); 30 | export const hhastPath = 31 | hackConfig.get("hhastPath") || "/vendor/bin/hhast-lint"; 32 | export const hhastArgs = hackConfig.get("hhastArgs", []); 33 | 34 | export const remoteEnabled = hackConfig.get("remote.enabled", false); 35 | export const remoteType: "ssh" | "docker" | undefined = hackConfig.get( 36 | "remote.type", 37 | undefined, 38 | ); 39 | export const remoteWorkspacePath = hackConfig.get( 40 | "remote.workspacePath", 41 | ); 42 | export const sshHost = hackConfig.get("remote.ssh.host", ""); 43 | export const sshArgs = hackConfig.get("remote.ssh.args", []); 44 | export const dockerContainerName = hackConfig.get( 45 | "remote.docker.containerName", 46 | "", 47 | ); 48 | 49 | // Prompt to reload workspace on certain configuration updates 50 | vscode.workspace.onDidChangeConfiguration(async (event) => { 51 | if (event.affectsConfiguration("hack.remote")) { 52 | const selection = await vscode.window.showInformationMessage( 53 | "Please reload your workspace to apply the latest Hack configuration changes.", 54 | { modal: true }, 55 | "Reload", 56 | ); 57 | if (selection === "Reload") { 58 | vscode.commands.executeCommand("workbench.action.reloadWindow"); 59 | } 60 | } 61 | }); 62 | -------------------------------------------------------------------------------- /src/HhvmDebugConfigurationProvider.ts: -------------------------------------------------------------------------------- 1 | import * as config from "./Config"; 2 | import { 3 | WorkspaceFolder, 4 | DebugConfigurationProvider, 5 | DebugConfiguration, 6 | CancellationToken, 7 | ProviderResult, 8 | } from "vscode"; 9 | 10 | export class HhvmDebugConfigurationProvider 11 | implements DebugConfigurationProvider 12 | { 13 | resolveDebugConfiguration( 14 | folder: WorkspaceFolder | undefined, 15 | debugConfig: DebugConfiguration, 16 | _token?: CancellationToken, 17 | ): ProviderResult { 18 | // if launch.json is missing or empty 19 | if ( 20 | !debugConfig.type || 21 | !debugConfig.request || 22 | !debugConfig.name || 23 | !folder 24 | ) { 25 | return undefined; 26 | } 27 | 28 | if (debugConfig.type === "hhvm" && debugConfig.request === "launch") { 29 | if (!debugConfig.script) { 30 | debugConfig.script = "${file}"; 31 | } 32 | 33 | debugConfig.localWorkspaceRoot = folder.uri.fsPath; 34 | debugConfig.remoteEnabled = config.remoteEnabled; 35 | debugConfig.remoteType = config.remoteType; 36 | debugConfig.remoteWorkspacePath = config.remoteWorkspacePath; 37 | debugConfig.dockerContainerName = config.dockerContainerName; 38 | } 39 | 40 | return debugConfig; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/LSPHHASTLint.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Integration with The HHAST Linter via the LSP 3 | */ 4 | 5 | import * as fs from "fs"; 6 | import * as vscode from "vscode"; 7 | import { 8 | HandleDiagnosticsSignature, 9 | LanguageClient, 10 | } from "vscode-languageclient/node"; 11 | import * as config from "./Config"; 12 | import * as remote from "./remote"; 13 | import * as utils from "./Utils"; 14 | 15 | type LintMode = "whole-project" | "open-files"; 16 | type InitializationOptions = { 17 | lintMode?: LintMode; 18 | }; 19 | 20 | export class LSPHHASTLint { 21 | private context: vscode.ExtensionContext; 22 | private hhastPath: string; 23 | 24 | constructor(context: vscode.ExtensionContext, hhastPath: string) { 25 | this.context = context; 26 | this.hhastPath = 27 | config.remoteEnabled && config.remoteWorkspacePath 28 | ? hhastPath.replace( 29 | config.localWorkspacePath, 30 | config.remoteWorkspacePath 31 | ) 32 | : hhastPath; 33 | } 34 | 35 | /** Start if HHAST support is enabled, the project uses HHAST, and the user 36 | * enables HHAST support for this project. 37 | */ 38 | public static async START_IF_CONFIGURED_AND_ENABLED( 39 | context: vscode.ExtensionContext 40 | ): Promise { 41 | if (!config.useHhast) { 42 | return; 43 | } 44 | const workspace = config.localWorkspacePath; 45 | const usesLint: boolean = await new Promise((resolve, _) => 46 | fs.access(`${workspace}/hhast-lint.json`, (err) => resolve(!err)) 47 | ); 48 | if (!usesLint) { 49 | return; 50 | } 51 | 52 | const rawHhastPath = config.hhastPath; 53 | const hhastPath = 54 | rawHhastPath && rawHhastPath[0] !== "/" 55 | ? `${workspace}/${rawHhastPath}` 56 | : rawHhastPath; 57 | if (!hhastPath) { 58 | return; 59 | } 60 | const hhastExists: boolean = await new Promise((resolve, _) => 61 | fs.access(hhastPath, (err) => resolve(!err)) 62 | ); 63 | if (!hhastExists) { 64 | return; 65 | } 66 | 67 | await new LSPHHASTLint(context, hhastPath).run(); 68 | } 69 | 70 | public async run(): Promise { 71 | const initializationOptions: InitializationOptions = {}; 72 | const lintMode = config.hhastLintMode; 73 | if (lintMode) { 74 | initializationOptions.lintMode = lintMode; 75 | } 76 | 77 | const hhast = new LanguageClient( 78 | "hack", 79 | "HHAST", 80 | { 81 | command: remote.getCommand(this.hhastPath), 82 | args: remote.getArgs(this.hhastPath, [ 83 | ...config.hhastArgs, 84 | "--mode", 85 | "lsp", 86 | "--from", 87 | "vscode-hack", 88 | ]), 89 | }, 90 | { 91 | documentSelector: [{ language: "hack", scheme: "file" }], 92 | initializationOptions: initializationOptions, 93 | uriConverters: { 94 | code2Protocol: utils.mapFromWorkspaceUri, 95 | protocol2Code: utils.mapToWorkspaceUri, 96 | }, 97 | middleware: { 98 | handleDiagnostics: this.handleDiagnostics, 99 | }, 100 | } 101 | ); 102 | await hhast.start(); 103 | this.context.subscriptions.push(hhast); 104 | } 105 | 106 | private handleDiagnostics( 107 | uri: vscode.Uri, 108 | diagnostics: vscode.Diagnostic[], 109 | next: HandleDiagnosticsSignature 110 | ) { 111 | next( 112 | uri, 113 | diagnostics.map((d) => { 114 | d.message = `${d.code}: ${d.message}`; 115 | return d; 116 | }) 117 | ); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/LSPHackTypeChecker.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Integration with the Hack type checker via the LSP 3 | */ 4 | 5 | import * as vscode from "vscode"; 6 | import { 7 | HandleDiagnosticsSignature, 8 | LanguageClient, 9 | RevealOutputChannelOn, 10 | LanguageClientOptions, 11 | ServerOptions, 12 | Command, 13 | } from "vscode-languageclient/node"; 14 | import * as config from "./Config"; 15 | import { HackCoverageChecker } from "./coveragechecker"; 16 | import * as remote from "./remote"; 17 | import * as hack from "./types/hack"; 18 | import * as utils from "./Utils"; 19 | import { ShowStatusRequest } from "./types/lsp"; 20 | import * as hh_client from "./proxy"; 21 | 22 | export class LSPHackTypeChecker { 23 | private context: vscode.ExtensionContext; 24 | private versionText: string; 25 | private status: vscode.StatusBarItem; 26 | 27 | constructor(context: vscode.ExtensionContext, version: hack.Version) { 28 | this.context = context; 29 | this.versionText = this.getVersionText(version); 30 | this.status = vscode.window.createStatusBarItem( 31 | vscode.StatusBarAlignment.Right, 32 | 1000 33 | ); 34 | this.status.name = "Hack Language Server"; 35 | context.subscriptions.push(this.status); 36 | } 37 | 38 | public static IS_SUPPORTED(version: hack.Version): boolean { 39 | return version.api_version >= 5; 40 | } 41 | 42 | public async run(): Promise { 43 | const context = this.context; 44 | 45 | // this.status.text = "$(alert) " + this.versionText; 46 | // this.status.tooltip = "hh_server is not running for this workspace."; 47 | // this.status.show(); 48 | context.subscriptions.push( 49 | vscode.commands.registerCommand("hack.restartLSP", async () => { 50 | await hh_client.start(); 51 | }) 52 | ); 53 | 54 | const serverOptions: ServerOptions = { 55 | command: remote.getCommand(config.clientPath), 56 | args: remote.getArgs(config.clientPath, ["lsp", "--from", "vscode-hack"]), 57 | }; 58 | 59 | const clientOptions: LanguageClientOptions = { 60 | documentSelector: [{ language: "hack", scheme: "file" }], 61 | initializationOptions: { useTextEditAutocomplete: true }, 62 | uriConverters: { 63 | code2Protocol: utils.mapFromWorkspaceUri, 64 | protocol2Code: utils.mapToWorkspaceUri, 65 | }, 66 | middleware: { 67 | handleDiagnostics: (uri, diagnostics, next) => { 68 | LSPHackTypeChecker.handleDiagnostics( 69 | uri, 70 | diagnostics, 71 | next, 72 | this.status 73 | ); 74 | }, 75 | }, 76 | // Hack returns errors if commands fail due to syntax errors. Don't 77 | // automatically switch to the Output pane in this case. 78 | revealOutputChannelOn: RevealOutputChannelOn.Never, 79 | }; 80 | 81 | const languageClient = new LanguageClient( 82 | "hack", 83 | "Hack Language Server", 84 | serverOptions, 85 | clientOptions 86 | ); 87 | 88 | languageClient.onRequest( 89 | "window/showStatus", 90 | (params: ShowStatusRequest) => { 91 | if (params.shortMessage) { 92 | this.status.text = this.versionText + " " + params.shortMessage; 93 | } else { 94 | this.status.text = this.versionText; 95 | } 96 | this.status.tooltip = params.message || ""; 97 | 98 | if (params.type === 1 || params.type === 2) { 99 | this.status.text = "$(alert) " + this.status.text; 100 | } 101 | 102 | this.status.show(); 103 | return {}; 104 | } 105 | ); 106 | 107 | await languageClient.start(); 108 | this.context.subscriptions.push(languageClient); 109 | 110 | if ( 111 | config.enableCoverageCheck && 112 | languageClient.initializeResult && 113 | (languageClient.initializeResult.capabilities).typeCoverageProvider 114 | ) { 115 | await new HackCoverageChecker(languageClient).start(context); 116 | } 117 | } 118 | 119 | private static handleDiagnostics( 120 | uri: vscode.Uri, 121 | diagnostics: vscode.Diagnostic[], 122 | next: HandleDiagnosticsSignature, 123 | status: vscode.StatusBarItem 124 | ) { 125 | // If the Hack LSP loses connectivity with hh_server, it publishes a special custom diagonstic event. Rather than 126 | // show it as a regular error, we instead capture it and add a more prominent status bar indicator instead. 127 | if ( 128 | diagnostics.length > 0 && 129 | diagnostics[0].source === "hh_server" && 130 | diagnostics[0].message.startsWith("hh_server isn't running") 131 | ) { 132 | status.backgroundColor = new vscode.ThemeColor( 133 | "statusBarItem.errorBackground" 134 | ); 135 | status.text = "$(error) Hack LSP"; 136 | status.tooltip = 137 | "Hack Language Server disconnected. Language features will be unavailable. Click to restart."; 138 | status.command = "hack.restartLSP"; 139 | status.show(); 140 | } else { 141 | // Handle regular errors 142 | status.hide(); 143 | next( 144 | uri, 145 | diagnostics.map((d) => { 146 | // See https://github.com/facebook/hhvm/blob/028402226993d53d68e17125e0b7c8dd87ea6c17/hphp/hack/src/errors/errors.ml#L174 147 | let kind: string; 148 | switch (Math.floor(d.code / 1000)) { 149 | case 1: 150 | kind = "Parsing"; 151 | break; 152 | case 2: 153 | kind = "Naming"; 154 | break; 155 | case 3: 156 | kind = "NastCheck"; 157 | break; 158 | case 4: 159 | kind = "Typing"; 160 | break; 161 | case 5: 162 | kind = "Lint"; 163 | break; 164 | case 8: 165 | kind = "Init"; 166 | break; 167 | default: 168 | kind = "Other"; 169 | } 170 | d.message = `${kind}[${d.code}] ${d.message}`; 171 | return d; 172 | }) 173 | ); 174 | } 175 | } 176 | 177 | private getVersionText(version: hack.Version): string { 178 | const hhvmVersion = version.commit.split("-").pop(); 179 | let statusText = hhvmVersion ? `HHVM ${hhvmVersion}` : "HHVM"; 180 | if (config.remoteEnabled && config.remoteType === "ssh") { 181 | statusText += " (Remote)"; 182 | } else if (config.remoteEnabled && config.remoteType === "docker") { 183 | statusText += " (Docker)"; 184 | } 185 | return statusText; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/LegacyHackTypeChecker.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file VS Code diagnostics integration with Hack typechecker by executing 3 | * `hh_client` each time 4 | */ 5 | 6 | import * as vscode from "vscode"; 7 | import * as providers from "./providers"; 8 | import * as hh_client from "./proxy"; 9 | import * as utils from "./Utils"; 10 | 11 | export class LegacyHackTypeChecker { 12 | private hhvmTypeDiag: vscode.DiagnosticCollection; 13 | 14 | constructor(context: vscode.ExtensionContext) { 15 | // register language functionality providers 16 | const HACK_MODE: vscode.DocumentFilter = { 17 | language: "hack", 18 | scheme: "file", 19 | }; 20 | context.subscriptions.push( 21 | vscode.languages.registerHoverProvider( 22 | HACK_MODE, 23 | new providers.HackHoverProvider(), 24 | ), 25 | ); 26 | context.subscriptions.push( 27 | vscode.languages.registerDocumentSymbolProvider( 28 | HACK_MODE, 29 | new providers.HackDocumentSymbolProvider(), 30 | ), 31 | ); 32 | context.subscriptions.push( 33 | vscode.languages.registerWorkspaceSymbolProvider( 34 | new providers.HackWorkspaceSymbolProvider(), 35 | ), 36 | ); 37 | context.subscriptions.push( 38 | vscode.languages.registerDocumentHighlightProvider( 39 | HACK_MODE, 40 | new providers.HackDocumentHighlightProvider(), 41 | ), 42 | ); 43 | context.subscriptions.push( 44 | vscode.languages.registerCompletionItemProvider( 45 | HACK_MODE, 46 | new providers.HackCompletionItemProvider(), 47 | "$", 48 | ">", 49 | ":", 50 | "\\", 51 | ), 52 | ); 53 | context.subscriptions.push( 54 | vscode.languages.registerDocumentFormattingEditProvider( 55 | HACK_MODE, 56 | new providers.HackDocumentFormattingEditProvider(), 57 | ), 58 | ); 59 | context.subscriptions.push( 60 | vscode.languages.registerReferenceProvider( 61 | HACK_MODE, 62 | new providers.HackReferenceProvider(), 63 | ), 64 | ); 65 | context.subscriptions.push( 66 | vscode.languages.registerDefinitionProvider( 67 | HACK_MODE, 68 | new providers.HackDefinitionProvider(), 69 | ), 70 | ); 71 | 72 | // create typechecker and run when workspace is first loaded and on every file save 73 | const hhvmTypeDiag: vscode.DiagnosticCollection = 74 | vscode.languages.createDiagnosticCollection("hack_typecheck"); 75 | this.hhvmTypeDiag = hhvmTypeDiag; 76 | context.subscriptions.push( 77 | vscode.workspace.onDidSaveTextDocument(() => { 78 | this.run(); 79 | }), 80 | ); 81 | context.subscriptions.push(hhvmTypeDiag); 82 | } 83 | 84 | public async run(): Promise { 85 | const typecheckResult = await hh_client.check(); 86 | this.hhvmTypeDiag.clear(); 87 | 88 | if (!typecheckResult || typecheckResult.passed) { 89 | return; 90 | } 91 | 92 | const diagnosticMap: Map = new Map(); 93 | typecheckResult.errors.forEach((error) => { 94 | const diagnostic = new vscode.Diagnostic( 95 | new vscode.Range( 96 | new vscode.Position( 97 | error.message[0].line - 1, 98 | error.message[0].start - 1, 99 | ), 100 | new vscode.Position(error.message[0].line - 1, error.message[0].end), 101 | ), 102 | `${error.message[0].descr} [${error.message[0].code}]`, 103 | vscode.DiagnosticSeverity.Error, 104 | ); 105 | 106 | diagnostic.code = error.message[0].code; 107 | diagnostic.source = "Hack"; 108 | 109 | const relatedInformation: vscode.DiagnosticRelatedInformation[] = []; 110 | for (let i = 1; i < error.message.length; i++) { 111 | relatedInformation.push( 112 | new vscode.DiagnosticRelatedInformation( 113 | new vscode.Location( 114 | utils.mapToWorkspaceUri(error.message[i].path), 115 | new vscode.Range( 116 | new vscode.Position( 117 | error.message[i].line - 1, 118 | error.message[i].start - 1, 119 | ), 120 | new vscode.Position( 121 | error.message[i].line - 1, 122 | error.message[i].end, 123 | ), 124 | ), 125 | ), 126 | error.message[i].descr, 127 | ), 128 | ); 129 | } 130 | diagnostic.relatedInformation = relatedInformation; 131 | 132 | const file = utils.mapToWorkspaceUri(error.message[0].path); 133 | const cachedFileDiagnostics = diagnosticMap.get(file); 134 | if (cachedFileDiagnostics) { 135 | cachedFileDiagnostics.push(diagnostic); 136 | } else { 137 | diagnosticMap.set(file, [diagnostic]); 138 | } 139 | }); 140 | diagnosticMap.forEach((diags, file) => { 141 | this.hhvmTypeDiag.set(file, diags); 142 | }); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/Utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Utility functions to help with file path parsing and others 3 | */ 4 | 5 | import * as vscode from "vscode"; 6 | import * as config from "./Config"; 7 | 8 | const localPath = getUriFilePath(config.localWorkspacePath); 9 | const remotePath = getUriFilePath(config.remoteWorkspacePath); 10 | 11 | function getUriFilePath(path: string | undefined): string { 12 | return path ? vscode.Uri.file(path).toString() : ""; 13 | } 14 | 15 | /** 16 | * Converts a local workspace URI to a file path string (with or without scheme) to pass to 17 | * the typechecker. Path is mapped to an alternate workspace root if configured. 18 | * @param file The file URI to convert 19 | * @param includeScheme Whether to include the file:// scheme in the response or not 20 | */ 21 | export const mapFromWorkspaceUri = (file: vscode.Uri): string => { 22 | if (!config.remoteEnabled || !config.remoteWorkspacePath) { 23 | return file.toString(); 24 | } 25 | return file.toString().replace(localPath, remotePath); 26 | }; 27 | 28 | /** 29 | * Converts a file path string received from the typechecker to a local workspace URI. 30 | * Path is mapped from an alternate workspace root if configured. 31 | * @param file The file path to convert 32 | */ 33 | export const mapToWorkspaceUri = (file: string): vscode.Uri => { 34 | let filePath = file; 35 | if (config.remoteEnabled && config.remoteWorkspacePath) { 36 | filePath = filePath.replace(remotePath, localPath); 37 | } 38 | if (filePath.startsWith("file://")) { 39 | return vscode.Uri.parse(filePath); 40 | } else { 41 | return vscode.Uri.file(filePath); 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /src/coveragechecker.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Logic to calculate Hack coverage percentage of a source file. 3 | */ 4 | 5 | import * as vscode from "vscode"; 6 | import { LanguageClient } from "vscode-languageclient/node"; 7 | import { TypeCoverageResponse } from "./types/lsp"; 8 | 9 | export class HackCoverageChecker { 10 | // whether coverage errors are highlighted in code and visible in the "Problems" tab 11 | private visible: boolean = false; 12 | 13 | // the percentage coverage indicator in the status bar 14 | private coverageStatus: vscode.StatusBarItem; 15 | 16 | // the global coverage error collection 17 | private hhvmCoverDiag: vscode.DiagnosticCollection; 18 | 19 | // the global hack language client instance 20 | private languageClient: LanguageClient; 21 | 22 | constructor(languageClient: LanguageClient) { 23 | this.coverageStatus = vscode.window.createStatusBarItem( 24 | vscode.StatusBarAlignment.Left 25 | ); 26 | this.hhvmCoverDiag = 27 | vscode.languages.createDiagnosticCollection("hack_coverage"); 28 | this.languageClient = languageClient; 29 | } 30 | 31 | public async start(context: vscode.ExtensionContext) { 32 | context.subscriptions.push( 33 | vscode.workspace.onDidSaveTextDocument(async (doc) => this.check(doc)) 34 | ); 35 | context.subscriptions.push( 36 | vscode.window.onDidChangeActiveTextEditor(async (editor) => { 37 | if (editor) { 38 | await this.check(editor.document); 39 | } else { 40 | this.hhvmCoverDiag.clear(); 41 | this.coverageStatus.hide(); 42 | } 43 | }) 44 | ); 45 | context.subscriptions.push( 46 | vscode.commands.registerCommand( 47 | "hack.toggleCoverageHighlight", 48 | async () => this.toggle() 49 | ) 50 | ); 51 | context.subscriptions.push(this.hhvmCoverDiag, this.coverageStatus); 52 | 53 | // Check the active file, if any 54 | if (vscode.window.activeTextEditor) { 55 | await this.check(vscode.window.activeTextEditor.document); 56 | } 57 | } 58 | 59 | public async toggle() { 60 | if (this.visible) { 61 | this.hhvmCoverDiag.clear(); 62 | this.visible = false; 63 | } else { 64 | this.visible = true; 65 | const editor = vscode.window.activeTextEditor; 66 | if (editor) { 67 | await this.check(editor.document); 68 | } 69 | } 70 | } 71 | 72 | private async check(document: vscode.TextDocument) { 73 | this.hhvmCoverDiag.clear(); 74 | if (document.languageId !== "hack" || document.uri.scheme !== "file") { 75 | this.coverageStatus.hide(); 76 | return; 77 | } 78 | 79 | let coverageResponse: TypeCoverageResponse; 80 | try { 81 | coverageResponse = 82 | await this.languageClient.sendRequest( 83 | "textDocument/typeCoverage", 84 | { 85 | textDocument: 86 | this.languageClient.code2ProtocolConverter.asTextDocumentIdentifier( 87 | document 88 | ), 89 | } 90 | ); 91 | } catch (e) { 92 | this.coverageStatus.hide(); 93 | return; 94 | } 95 | 96 | this.coverageStatus.text = `$(paintcan) ${coverageResponse.coveredPercent}%`; 97 | this.coverageStatus.tooltip = `This file is ${coverageResponse.coveredPercent}% covered by Hack.\nClick to toggle highlighting of uncovered areas.`; 98 | this.coverageStatus.command = "hack.toggleCoverageHighlight"; 99 | this.coverageStatus.show(); 100 | 101 | if (this.visible && coverageResponse.uncoveredRanges) { 102 | const diagnostics: vscode.Diagnostic[] = []; 103 | coverageResponse.uncoveredRanges.forEach((uncoveredRange) => { 104 | const diagnostic = new vscode.Diagnostic( 105 | uncoveredRange.range, 106 | uncoveredRange.message || coverageResponse.defaultMessage, 107 | vscode.DiagnosticSeverity.Information 108 | ); 109 | diagnostic.source = "Type Coverage"; 110 | diagnostics.push(diagnostic); 111 | }); 112 | this.hhvmCoverDiag.set(document.uri, diagnostics); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/debugger.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file VSCode HHVM Debugger Adapter 3 | * 4 | * HHVM already speaks the vscode debugger protocol, so ideally the editor could directly 5 | * launch or attach to a HHVM process. However, vscode expects debugger communication 6 | * through stdin/stdout while HHVM needs those for running the program itself and instead 7 | * exposes the debugger over a TCP port. This adapter is a thin Node executable that connects 8 | * the two. 9 | * 10 | */ 11 | 12 | import * as child_process from "child_process"; 13 | import { SpawnOptionsWithoutStdio } from "child_process"; 14 | import * as http from "http"; 15 | import * as net from "net"; 16 | import * as os from "os"; 17 | import { Writable } from "stream"; 18 | import { OutputEvent } from "@vscode/debugadapter"; 19 | import { DebugProtocol } from "@vscode/debugprotocol"; 20 | 21 | const TWO_CRLF = "\r\n\r\n"; 22 | const CONTENT_LENGTH_PATTERN = new RegExp("Content-Length: (\\d+)"); 23 | const DEFAULT_HHVM_DEBUGGER_PORT = 8999; 24 | const DEFAULT_HHVM_PATH = "hhvm"; 25 | 26 | type DebuggerWriteCallback = (data: string) => void; 27 | 28 | interface HhvmAttachRequestArguments 29 | extends DebugProtocol.AttachRequestArguments { 30 | host?: string; 31 | port?: string; 32 | socket?: string; 33 | remoteSiteRoot?: string; 34 | localWorkspaceRoot?: string; 35 | sandboxUser?: string; 36 | launchUrl?: string; 37 | } 38 | 39 | interface HhvmLaunchRequestArguments 40 | extends DebugProtocol.LaunchRequestArguments { 41 | hhvmPath?: string; 42 | hhvmArgs?: string[]; 43 | script?: string; 44 | cwd?: string; 45 | deferLaunch?: boolean; 46 | sandboxUser?: string; 47 | localWorkspaceRoot?: string; 48 | remoteEnabled?: boolean; 49 | remoteType?: string; 50 | remoteWorkspacePath?: string; 51 | dockerContainerName?: string; 52 | } 53 | 54 | class HHVMDebuggerWrapper { 55 | private sequenceNumber: number; 56 | private currentOutputData: string; 57 | private currentInputData: string; 58 | private currentContentLength: number; 59 | private bufferedRequests: DebugProtocol.Request[]; 60 | private debugging: boolean; 61 | private debuggerWriteCallback?: DebuggerWriteCallback; 62 | private nonLoaderBreakSeen: boolean; 63 | private initializeArgs: DebugProtocol.InitializeRequestArguments; 64 | // private runInTerminalRequest: Promise | undefined; 65 | private asyncBreakPending: boolean; 66 | 67 | private remoteSiteRoot: string | undefined; 68 | private remoteSiteRootPattern: RegExp | undefined; 69 | private localWorkspaceRoot: string | undefined; 70 | private localWorkspaceRootPattern: RegExp | undefined; 71 | 72 | constructor() { 73 | this.sequenceNumber = 0; 74 | this.currentContentLength = 0; 75 | this.currentOutputData = ""; 76 | this.currentInputData = ""; 77 | this.bufferedRequests = []; 78 | this.debugging = false; 79 | this.nonLoaderBreakSeen = false; 80 | this.initializeArgs = { adapterID: "hhvm" }; 81 | this.asyncBreakPending = false; 82 | } 83 | 84 | public debug() { 85 | process.stdin.on("data", (chunk) => { 86 | this.processClientMessage(chunk); 87 | }); 88 | 89 | process.on("disconnect", () => { 90 | process.exit(); 91 | }); 92 | } 93 | 94 | private attachTarget( 95 | attachMessage: DebugProtocol.AttachRequest, 96 | retries: number = 0, 97 | ) { 98 | const args: HhvmAttachRequestArguments = attachMessage.arguments; 99 | const attachPort = args.port 100 | ? parseInt(args.port, 10) 101 | : DEFAULT_HHVM_DEBUGGER_PORT; 102 | 103 | this.remoteSiteRoot = args.remoteSiteRoot; 104 | this.remoteSiteRootPattern = args.remoteSiteRoot 105 | ? new RegExp(this.escapeRegExp(args.remoteSiteRoot), "g") 106 | : undefined; 107 | this.localWorkspaceRoot = args.localWorkspaceRoot; 108 | this.localWorkspaceRootPattern = args.localWorkspaceRoot 109 | ? new RegExp(this.escapeRegExp(args.localWorkspaceRoot), "g") 110 | : undefined; 111 | 112 | if (!args.socket && Number.isNaN(attachPort)) { 113 | throw new Error("Invalid HHVM debug port or socket path."); 114 | } 115 | 116 | if (!args.sandboxUser || args.sandboxUser.trim() === "") { 117 | args.sandboxUser = os.userInfo().username; 118 | } 119 | 120 | const socketArgs = args.socket 121 | ? { path: args.socket } 122 | : { port: attachPort }; 123 | const socket = net.createConnection(socketArgs); 124 | 125 | socket.on("data", (chunk) => { 126 | this.processDebuggerMessage(chunk); 127 | }); 128 | 129 | socket.on("close", () => { 130 | this.writeOutputWithHeader({ 131 | seq: ++this.sequenceNumber, 132 | type: "event", 133 | event: "hhvmConnectionDied", 134 | }); 135 | process.stderr.write( 136 | "The connection to the debug target has been closed.", 137 | ); 138 | process.exit(0); 139 | }); 140 | 141 | const callback = (data: string) => { 142 | // Map local workspace file paths to server root, if needed 143 | let mappedData = data; 144 | if (this.localWorkspaceRootPattern && this.remoteSiteRoot) { 145 | mappedData = mappedData.replace( 146 | this.localWorkspaceRootPattern, 147 | this.remoteSiteRoot, 148 | ); 149 | } 150 | 151 | socket.write(`${mappedData}\0`, "utf8"); 152 | }; 153 | 154 | callback(JSON.stringify(attachMessage)); 155 | this.debuggerWriteCallback = callback; 156 | this.forwardBufferedMessages(); 157 | this.debugging = true; 158 | 159 | const attachResponse = { 160 | request_seq: attachMessage.seq, 161 | success: true, 162 | command: attachMessage.command, 163 | }; 164 | this.writeResponseMessage(attachResponse); 165 | 166 | socket.on("error", (error) => { 167 | if (retries >= 5) { 168 | process.stderr.write( 169 | `Error communicating with debugger target: ${error.toString()}\n`, 170 | ); 171 | process.exit((error).code); 172 | } else { 173 | // When reconnecting to a target we just disconnected from, especially 174 | // in the case of an unclean disconnection, it may take a moment 175 | // for HHVM to receive a TCP socket error and realize the client is 176 | // gone. Rather than failing to reconnect, wait a moment and try 177 | // again to provide a better user experience. 178 | setTimeout(() => this.attachTarget(attachMessage, retries + 1), 2000); 179 | } 180 | }); 181 | 182 | if (args.launchUrl) { 183 | http 184 | .get(args.launchUrl, (res) => { 185 | res.on("data", (data) => { 186 | this.writeOutputEvent("stdout", data.toString()); 187 | }); 188 | res.on("end", () => { 189 | process.exit(); 190 | }); 191 | }) 192 | .on("error", (e) => { 193 | this.writeOutputEvent("stdout", e.message); 194 | process.exit(); 195 | }); 196 | } 197 | } 198 | 199 | private launchTarget(launchMessage: DebugProtocol.LaunchRequest) { 200 | const args: HhvmLaunchRequestArguments = launchMessage.arguments; 201 | 202 | if (args.deferLaunch) { 203 | this.launchTargetInTerminal(launchMessage); 204 | return; 205 | } 206 | 207 | let hhvmPath = args.hhvmPath; 208 | if (!hhvmPath || hhvmPath === "") { 209 | hhvmPath = DEFAULT_HHVM_PATH; 210 | } 211 | 212 | if (!args.sandboxUser || args.sandboxUser.trim() === "") { 213 | args.sandboxUser = os.userInfo().username; 214 | } 215 | 216 | const hhvmArgs = args.hhvmArgs || []; 217 | let scriptArgs = args.script ? args.script.split(" ") : []; 218 | const dockerArgs: string[] = []; 219 | if ( 220 | args.remoteEnabled && 221 | args.remoteType === "docker" && 222 | args.dockerContainerName 223 | ) { 224 | hhvmPath = "docker"; 225 | dockerArgs.push("exec", "-i", args.dockerContainerName, "hhvm"); 226 | scriptArgs = scriptArgs.map((value) => 227 | value.replace( 228 | args.localWorkspaceRoot || "", 229 | args.remoteWorkspacePath || "", 230 | ), 231 | ); 232 | 233 | this.remoteSiteRoot = args.remoteWorkspacePath; 234 | this.remoteSiteRootPattern = args.remoteWorkspacePath 235 | ? new RegExp(this.escapeRegExp(args.remoteWorkspacePath), "g") 236 | : undefined; 237 | this.localWorkspaceRoot = args.localWorkspaceRoot; 238 | this.localWorkspaceRootPattern = args.localWorkspaceRoot 239 | ? new RegExp(this.escapeRegExp(args.localWorkspaceRoot), "g") 240 | : undefined; 241 | } 242 | 243 | const allArgs = [ 244 | ...dockerArgs, 245 | "--mode", 246 | "vsdebug", 247 | ...hhvmArgs, 248 | ...scriptArgs, 249 | ]; 250 | const options: SpawnOptionsWithoutStdio = { 251 | cwd: args.cwd || process.cwd(), 252 | // FD[3] is used for communicating with the debugger extension. 253 | // STDIN, STDOUT and STDERR are the actual PHP streams. 254 | // If launchMessage.noDebug is specified, start the child but don't 255 | // connect the debugger fd pipe. 256 | stdio: Boolean(args.noDebug) 257 | ? ["pipe", "pipe", "pipe"] 258 | : ["pipe", "pipe", "pipe", "pipe"], 259 | // When the wrapper exits, so does the target. 260 | detached: false, 261 | env: process.env, 262 | }; 263 | 264 | const targetProcess = child_process.spawn(hhvmPath, allArgs, options); 265 | 266 | // Exit with the same error code the target exits with. 267 | targetProcess.on("exit", (code) => process.exit(code || undefined)); 268 | targetProcess.on("error", (error) => 269 | process.stderr.write(`${error.toString()}\n`), 270 | ); 271 | 272 | // Wrap any stdout from the target into a VS Code stdout event. 273 | targetProcess.stdout.on("data", (chunk) => { 274 | const block: string = chunk.toString(); 275 | this.writeOutputEvent("stdout", block); 276 | }); 277 | targetProcess.stdout.on("error", () => {}); 278 | 279 | // Wrap any stderr from the target into a VS Code stderr event. 280 | targetProcess.stderr.on("data", (chunk) => { 281 | const block: string = chunk.toString(); 282 | this.writeOutputEvent("stderr", block); 283 | }); 284 | targetProcess.stderr.on("error", () => {}); 285 | 286 | if (targetProcess.stdio[3]) { 287 | targetProcess.stdio[3].on("data", (chunk) => { 288 | this.processDebuggerMessage(chunk); 289 | }); 290 | targetProcess.stdio[3].on("error", () => {}); 291 | } 292 | 293 | // Read data from the debugger client on stdin and forward to the 294 | // debugger engine in the target. 295 | const callback = (data: string) => { 296 | // Map local workspace file paths to server root, if needed 297 | let mappedData = data; 298 | if (this.localWorkspaceRootPattern && this.remoteSiteRoot) { 299 | mappedData = mappedData.replace( 300 | this.localWorkspaceRootPattern, 301 | this.remoteSiteRoot, 302 | ); 303 | } 304 | 305 | (targetProcess.stdio[3]).write(`${mappedData}\0`, "utf8"); 306 | }; 307 | 308 | callback(JSON.stringify(launchMessage)); 309 | this.debuggerWriteCallback = callback; 310 | this.forwardBufferedMessages(); 311 | this.debugging = true; 312 | } 313 | 314 | private launchTargetInTerminal(requestMessage: DebugProtocol.LaunchRequest) { 315 | // This is a launch in terminal request. Perform the launch and then 316 | // return an attach configuration. 317 | const startupArgs: HhvmLaunchRequestArguments = requestMessage.arguments; 318 | 319 | let hhvmPath = startupArgs.hhvmPath; 320 | if (!hhvmPath || hhvmPath === "") { 321 | hhvmPath = DEFAULT_HHVM_PATH; 322 | } 323 | 324 | // Terminal args require everything to be a string, but debug port 325 | // is typed as a number. 326 | const terminalArgs = [hhvmPath]; 327 | if (startupArgs.hhvmArgs) { 328 | for (const arg of startupArgs.hhvmArgs) { 329 | terminalArgs.push(String(arg)); 330 | } 331 | } 332 | 333 | const runInTerminalArgs: DebugProtocol.RunInTerminalRequestArguments = { 334 | kind: "integrated", 335 | cwd: __dirname, 336 | args: terminalArgs, 337 | }; 338 | 339 | this.writeOutputWithHeader({ 340 | seq: ++this.sequenceNumber, 341 | type: "request", 342 | command: "runInTerminal", 343 | arguments: runInTerminalArgs, 344 | }); 345 | // this.runInTerminalRequest = new Promise(); 346 | // this.runInTerminalRequest.promise; 347 | 348 | const attachConfig: HhvmAttachRequestArguments = { 349 | // debugPort: startupArgs.debugPort, 350 | sandboxUser: startupArgs.sandboxUser, 351 | }; 352 | this.attachTarget({ ...requestMessage, arguments: attachConfig }); 353 | } 354 | 355 | private forwardBufferedMessages() { 356 | if (this.debuggerWriteCallback) { 357 | const callback = this.debuggerWriteCallback; 358 | for (const requestMsg of this.bufferedRequests) { 359 | callback(JSON.stringify(requestMsg)); 360 | } 361 | } 362 | } 363 | 364 | private processClientMessage(chunk: Buffer) { 365 | this.currentInputData += chunk.toString(); 366 | while (true) { 367 | if (this.currentContentLength === 0) { 368 | // Look for a content length header. 369 | this.readContentHeader(); 370 | } 371 | 372 | const length = this.currentContentLength; 373 | if (length === 0 || this.currentInputData.length < length) { 374 | // We're not expecting a message, or the amount of data we have 375 | // available is smaller than the expected message. Wait for more data. 376 | break; 377 | } 378 | 379 | const message = this.currentInputData.substr(0, length); 380 | const requestMsg = JSON.parse(message); 381 | if (!this.handleWrapperRequest(requestMsg)) { 382 | const callback = this.debuggerWriteCallback; 383 | if (callback) { 384 | callback(this.translateNuclideRequest(requestMsg)); 385 | } 386 | } 387 | 388 | // Reset state and expect another content length header next. 389 | this.currentContentLength = 0; 390 | this.currentInputData = this.currentInputData.substr(length); 391 | } 392 | } 393 | 394 | private translateNuclideRequest(requestMsg: DebugProtocol.Request): string { 395 | // Nuclide has some extension messages that are not actually part of the 396 | // VS Code Debug protocol. These are prefixed with "nuclide_" to indicate 397 | // that they are non-standard requests. Since the HHVM side is agnostic 398 | // to what IDE it is talking to, these same commands (if they are available) 399 | // are actually prefixed with a more generic 'fb_' so convert. 400 | if (requestMsg.command && requestMsg.command.startsWith("nuclide_")) { 401 | requestMsg.command = requestMsg.command.replace("nuclide_", "fb_"); 402 | return JSON.stringify(requestMsg); 403 | } 404 | return JSON.stringify(requestMsg); 405 | } 406 | 407 | private handleWrapperRequest(requestMsg: DebugProtocol.Request): boolean { 408 | // Certain messages should be handled in the wrapper rather than forwarding 409 | // to HHVM. 410 | if (requestMsg.command) { 411 | switch (requestMsg.command) { 412 | case "initialize": 413 | this.initializeArgs = requestMsg.arguments; 414 | this.writeResponseMessage({ 415 | request_seq: requestMsg.seq, 416 | success: true, 417 | command: requestMsg.command, 418 | body: { 419 | exceptionBreakpointFilters: [ 420 | { 421 | default: false, 422 | label: "Break On All Exceptions", 423 | filter: "all", 424 | }, 425 | ], 426 | supportsLoadedSourcesRequest: false, 427 | supportTerminateDebuggee: false, 428 | supportsExceptionOptions: true, 429 | supportsModulesRequest: false, 430 | supportsHitConditionalBreakpoints: false, 431 | supportsConfigurationDoneRequest: true, 432 | supportsDelayedStackTraceLoading: true, 433 | supportsSetVariable: true, 434 | supportsGotoTargetsRequest: false, 435 | supportsExceptionInfoRequest: false, 436 | supportsValueFormattingOptions: true, 437 | supportsEvaluateForHovers: true, 438 | supportsRestartRequest: false, 439 | supportsConditionalBreakpoints: true, 440 | supportsStepBack: false, 441 | supportsCompletionsRequest: true, 442 | supportsRestartFrame: false, 443 | supportsStepInTargetsRequest: false, 444 | 445 | // Experimental support for terminate thread 446 | supportsTerminateThreadsRequest: true, 447 | }, 448 | }); 449 | break; 450 | case "disconnect": 451 | this.writeResponseMessage({ 452 | request_seq: requestMsg.seq, 453 | success: true, 454 | command: requestMsg.command, 455 | }); 456 | 457 | // Exit this process, which will also result in the child being killed 458 | // (in the case of Launch mode), or the socket to the child being 459 | // terminated (in the case of Attach mode). 460 | process.exit(0); 461 | return true; 462 | case "launch": 463 | this.launchTarget(requestMsg); 464 | return true; 465 | 466 | case "attach": 467 | this.attachTarget(requestMsg); 468 | return true; 469 | 470 | /*case 'runInTerminal': 471 | if (this.runInTerminalRequest) { 472 | if ((requestMsg).success) { 473 | this.runInTerminalRequest.resolve(); 474 | } else { 475 | this.runInTerminalRequest.reject(new Error(requestMsg.message)); 476 | } 477 | } 478 | return true; 479 | */ 480 | 481 | case "pause": { 482 | this.asyncBreakPending = true; 483 | return false; 484 | } 485 | 486 | default: 487 | } 488 | } 489 | 490 | if (!this.debugging) { 491 | // If debugging hasn't started yet, we need to buffer this request to 492 | // send to the backend once a connection has been established. 493 | this.bufferedRequests.push(requestMsg); 494 | return true; 495 | } 496 | 497 | return false; 498 | } 499 | 500 | private processDebuggerMessage(chunk: Buffer) { 501 | this.currentOutputData += chunk.toString(); 502 | 503 | // The messages from HHVM are each terminated by a NULL character. 504 | // Process any complete messages from HHVM. 505 | let idx = this.currentOutputData.indexOf("\0"); 506 | while (idx > 0) { 507 | const message = this.currentOutputData.substr(0, idx); 508 | 509 | // Add a sequence number to the data. 510 | try { 511 | const obj = JSON.parse(message); 512 | obj.seq = ++this.sequenceNumber; 513 | this.writeOutputWithHeader(obj); 514 | } catch (e: any) { 515 | process.stderr.write( 516 | `Error parsing message from target: ${e.toString()}: ${message}\n`, 517 | ); 518 | } 519 | 520 | // Advance to idx + 1 (lose the NULL char) 521 | this.currentOutputData = this.currentOutputData.substr(idx + 1); 522 | idx = this.currentOutputData.indexOf("\0"); 523 | } 524 | } 525 | 526 | private readContentHeader() { 527 | const idx = this.currentInputData.indexOf(TWO_CRLF); 528 | if (idx <= 0) { 529 | return; 530 | } 531 | 532 | const header = this.currentInputData.substr(0, idx); 533 | const match = header.match(CONTENT_LENGTH_PATTERN); 534 | if (!match) { 535 | throw new Error("Unable to parse message from debugger client"); 536 | } 537 | 538 | // Chop the Content-Length header off the input data and start looking for 539 | // the message. 540 | this.currentContentLength = parseInt(match[1], 10); 541 | this.currentInputData = this.currentInputData.substr(idx + TWO_CRLF.length); 542 | ++this.sequenceNumber; 543 | } 544 | 545 | private writeOutputEvent(eventType: string, message: string) { 546 | const outputEvent: OutputEvent = { 547 | seq: ++this.sequenceNumber, 548 | type: "event", 549 | event: "output", 550 | body: { 551 | category: eventType, 552 | output: message, 553 | }, 554 | }; 555 | this.writeOutputWithHeader(outputEvent); 556 | } 557 | 558 | private writeResponseMessage(message: {}) { 559 | this.writeOutputWithHeader({ 560 | seq: ++this.sequenceNumber, 561 | type: "response", 562 | ...message, 563 | }); 564 | } 565 | 566 | // Helper to apply compatibility fixes and errata to messages coming 567 | // from HHVM. Since we have the ability to update this wrapper much 568 | // more quickly than a new HHVM release, this allows us to modify 569 | // behavior and fix compatibility bugs before presenting the messages 570 | // to the client. 571 | private applyCompatabilityFixes(message: any) { 572 | if (message.type === "response") { 573 | switch (message.command) { 574 | case "threads": { 575 | if (Array.isArray(message.body)) { 576 | // TODO(ericblue): Fix threads response on the HHVM side. 577 | message.body = { threads: message.body }; 578 | } 579 | break; 580 | } 581 | case "stackTrace": { 582 | message.body.stackFrames.forEach((stackFrame: any) => { 583 | if (stackFrame.source != null) { 584 | if (stackFrame.source.path === "") { 585 | // TODO(ericblue): Delete source when there's none known. 586 | delete stackFrame.source; 587 | } /*else if (nuclideUri.isAbsolute(stackFrame.source.name)) { 588 | // TODO(ericblue): source names shouldn't be absolute paths. 589 | stackFrame.source.name = nuclideUri.basename( 590 | stackFrame.source.name, 591 | ); 592 | }*/ 593 | } 594 | }); 595 | break; 596 | } 597 | case "variables": { 598 | message.body.variables.forEach((variable: any) => { 599 | if (!("variablesReference" in variable)) { 600 | variable.variablesReference = 0; 601 | } 602 | }); 603 | break; 604 | } 605 | default: 606 | // No fixes needed. 607 | } 608 | } else if (message.type === "event") { 609 | switch (message.event) { 610 | case "output": { 611 | // Nuclide console requires all output messages to end with a newline 612 | // to work properly. 613 | if (message.body != null && !message.body.output.endsWith("\n")) { 614 | message.body.output += "\n"; 615 | } 616 | 617 | // Map custom output types onto explicit protocol types. 618 | switch (message.body.category) { 619 | case "warning": 620 | message.body.category = "console"; 621 | break; 622 | case "error": 623 | message.body.category = "stderr"; 624 | break; 625 | case "stdout": 626 | case "info": 627 | message.body.category = "log"; 628 | break; 629 | default: 630 | } 631 | break; 632 | } 633 | case "stopped": { 634 | // TODO: Ericblue: Remove this block once HHVM version that always 635 | // specifies preserveFocusHint is landed and the supported version. 636 | const reason = (message.body.reason || "") 637 | .toLowerCase() 638 | .split(" ")[0]; 639 | const focusThread = 640 | message.body.threadCausedFocus != null 641 | ? message.body.threadCausedFocus 642 | : reason === "step" || 643 | reason === "breakpoint" || 644 | reason === "exception"; 645 | 646 | if (message.body.preserveFocusHint == null) { 647 | message.body.preserveFocusHint = !focusThread; 648 | } 649 | 650 | break; 651 | } 652 | default: 653 | // No fixes needed. 654 | } 655 | } 656 | } 657 | 658 | private writeOutputWithHeader(message: any) { 659 | this.applyCompatabilityFixes(message); 660 | 661 | // TODO(ericblue): Fix breakpoint events format on the HHVM side. 662 | if (message.type === "event" && message.event === "breakpoint") { 663 | // * Skip 'new' & 'removed' breakpoint events as these weren't triggered by the backend. 664 | if (message.body.reason !== "changed") { 665 | return; 666 | } 667 | // * Change `breakpoint.column` to `1` insead of `0` 668 | message.body.breakpoint.column = 1; 669 | } 670 | 671 | if (message.type === "event" && message.event === "stopped") { 672 | if (!this.nonLoaderBreakSeen) { 673 | if ( 674 | message.body != null && 675 | message.body.description !== "execution paused" 676 | ) { 677 | // This is the first real (non-loader-break) stopped event. 678 | this.nonLoaderBreakSeen = true; 679 | } else if (!this.asyncBreakPending) { 680 | // Hide the loader break from Nuclide. 681 | return; 682 | } 683 | } 684 | 685 | this.asyncBreakPending = false; 686 | } 687 | 688 | // Skip forwarding non-focused thread stop events to VSCode's UI 689 | // to avoid confusing the UX on what thread to focus. 690 | // https://github.com/Microsoft/vscode-debugadapter-node/issues/147 691 | if ( 692 | message.type === "event" && 693 | message.event === "stopped" && 694 | (message.body.threadCausedFocus === false || 695 | message.body.preserveFocusHint === true) && 696 | this.initializeArgs.clientID !== "atom" 697 | ) { 698 | return; 699 | } 700 | 701 | let output = JSON.stringify(message); 702 | 703 | // Map server file paths to local workspace, if needed 704 | if (this.remoteSiteRootPattern && this.localWorkspaceRoot) { 705 | output = output.replace( 706 | this.remoteSiteRootPattern, 707 | this.localWorkspaceRoot, 708 | ); 709 | } 710 | 711 | const length = Buffer.byteLength(output, "utf8"); 712 | process.stdout.write(`Content-Length: ${length}${TWO_CRLF}`, "utf8"); 713 | process.stdout.write(output, "utf8"); 714 | } 715 | 716 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions 717 | private escapeRegExp(str: string) { 718 | return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string 719 | } 720 | } 721 | 722 | new HHVMDebuggerWrapper().debug(); 723 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Entry point for VS Code Hack extension. 3 | */ 4 | 5 | import * as vscode from "vscode"; 6 | import * as config from "./Config"; 7 | import { LegacyHackTypeChecker } from "./LegacyHackTypeChecker"; 8 | import { LSPHackTypeChecker } from "./LSPHackTypeChecker"; 9 | import { LSPHHASTLint } from "./LSPHHASTLint"; 10 | import * as hh_client from "./proxy"; 11 | import { HhvmDebugConfigurationProvider } from "./HhvmDebugConfigurationProvider"; 12 | 13 | export async function activate(context: vscode.ExtensionContext) { 14 | // check if a compatible verison of hh_client is installed, or show an error message and deactivate extension typecheck & intellisense features 15 | const version = await hh_client.version(); 16 | if (!version) { 17 | let errMsg = `Invalid hh_client executable: '${config.clientPath}'. Please ensure that HHVM is correctly installed or configure an alternate hh_client path in workspace settings.`; 18 | 19 | if (config.remoteEnabled && config.remoteType === "ssh") { 20 | errMsg = `Unable to connect to remote Hack server, please ensure it is running, and restart VSCode.`; 21 | } else if (config.remoteEnabled && config.remoteType === "docker") { 22 | errMsg = `Unable to connect to the HHVM Docker container, please ensure the container and HHVM is running, and restart VSCode.`; 23 | } 24 | 25 | vscode.window.showErrorMessage(errMsg); 26 | return; 27 | } 28 | 29 | await vscode.window.withProgress( 30 | { 31 | location: vscode.ProgressLocation.Window, 32 | title: `Running Hack typechecker`, 33 | }, 34 | async () => { 35 | return hh_client.start(); 36 | }, 37 | ); 38 | 39 | const services: Promise[] = []; 40 | services.push(LSPHHASTLint.START_IF_CONFIGURED_AND_ENABLED(context)); 41 | 42 | if (LSPHackTypeChecker.IS_SUPPORTED(version) && config.useLanguageServer) { 43 | services.push(new LSPHackTypeChecker(context, version).run()); 44 | } else { 45 | services.push(new LegacyHackTypeChecker(context).run()); 46 | } 47 | 48 | vscode.debug.registerDebugConfigurationProvider( 49 | "hhvm", 50 | new HhvmDebugConfigurationProvider(), 51 | ); 52 | 53 | await Promise.all(services); 54 | } 55 | 56 | export function deactivate() { 57 | // nothing to clean up 58 | } 59 | -------------------------------------------------------------------------------- /src/providers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Extension providers for intellisense features (hover, autocomplete, goto symbol etc.) 3 | */ 4 | 5 | import * as vscode from "vscode"; 6 | import * as hh_client from "./proxy"; 7 | import { OutlineResponse } from "./types/hack"; 8 | import * as utils from "./Utils"; 9 | 10 | const symbolArray = [ 11 | { key: "function", value: vscode.SymbolKind.Function }, 12 | { key: "method", value: vscode.SymbolKind.Method }, 13 | { key: "class", value: vscode.SymbolKind.Class }, 14 | { key: "const", value: vscode.SymbolKind.Constant }, 15 | { key: "interface", value: vscode.SymbolKind.Interface }, 16 | { key: "enum", value: vscode.SymbolKind.Enum }, 17 | { key: "trait", value: vscode.SymbolKind.Interface }, 18 | { key: "property", value: vscode.SymbolKind.Property }, 19 | ]; 20 | 21 | const symbolMap = new Map( 22 | symbolArray.map<[string, vscode.SymbolKind]>((x) => [x.key, x.value]), 23 | ); 24 | 25 | const getRange = ( 26 | lineStart: number, 27 | lineEnd: number, 28 | charStart: number, 29 | charEnd: number, 30 | ): vscode.Range => { 31 | return new vscode.Range( 32 | new vscode.Position(lineStart - 1, charStart - 1), 33 | new vscode.Position(lineEnd - 1, charEnd - 1), 34 | ); 35 | }; 36 | 37 | const getSymbolKind = (symbolType: string): vscode.SymbolKind => { 38 | return symbolMap.get(symbolType) || vscode.SymbolKind.Null; 39 | }; 40 | 41 | const pushSymbols = ( 42 | outline: OutlineResponse[], 43 | symbols: vscode.SymbolInformation[], 44 | container: string, 45 | indent: string, 46 | ) => { 47 | outline.forEach((element) => { 48 | let name = element.name; 49 | const nameIndex = name.lastIndexOf("\\"); 50 | if (nameIndex !== -1) { 51 | container = name.slice(0, nameIndex); 52 | name = name.slice(nameIndex + 1); 53 | } 54 | let symbolKind = getSymbolKind(element.kind); 55 | 56 | switch (symbolKind) { 57 | case vscode.SymbolKind.Method: 58 | case vscode.SymbolKind.Function: 59 | if (element.name === "__construct") { 60 | symbolKind = vscode.SymbolKind.Constructor; 61 | } 62 | name += "()"; 63 | break; 64 | default: 65 | } 66 | name = indent + name; 67 | const range = getRange( 68 | element.span.line_start, 69 | element.span.line_end, 70 | element.span.char_start, 71 | element.span.char_end, 72 | ); 73 | symbols.push( 74 | new vscode.SymbolInformation( 75 | name, 76 | symbolKind, 77 | range, 78 | undefined, 79 | container, 80 | ), 81 | ); 82 | 83 | // Check if element has any children, and recursively fetch them as well. 84 | // NOTE: Do DFS traversal here because we want the groups to be listed together. 85 | if (element.children && element.children.length > 0) { 86 | pushSymbols(element.children, symbols, name, `${indent} `); 87 | } 88 | }); 89 | }; 90 | 91 | export class HackHoverProvider implements vscode.HoverProvider { 92 | public provideHover( 93 | document: vscode.TextDocument, 94 | position: vscode.Position, 95 | ): vscode.ProviderResult { 96 | const wordPosition = document.getWordRangeAtPosition(position); 97 | if (!wordPosition) { 98 | return; 99 | } 100 | const startPosition = wordPosition.start; 101 | const line: number = startPosition.line + 1; 102 | const character: number = startPosition.character + 1; 103 | return hh_client 104 | .typeAtPos(utils.mapFromWorkspaceUri(document.uri), line, character) 105 | .then((hoverType) => { 106 | if (!hoverType) { 107 | return; 108 | } 109 | if (hoverType.startsWith("(function")) { 110 | hoverType = hoverType.slice(1, hoverType.length - 1); 111 | } 112 | const formattedMessage: vscode.MarkedString = { 113 | language: "hack", 114 | value: hoverType, 115 | }; 116 | return new vscode.Hover(formattedMessage); 117 | }); 118 | } 119 | } 120 | 121 | export class HackDocumentSymbolProvider 122 | implements vscode.DocumentSymbolProvider 123 | { 124 | public provideDocumentSymbols( 125 | document: vscode.TextDocument, 126 | ): vscode.ProviderResult { 127 | return hh_client.outline(document.getText()).then((outline) => { 128 | const symbols: vscode.SymbolInformation[] = []; 129 | pushSymbols(outline, symbols, "", ""); 130 | return symbols; 131 | }); 132 | } 133 | } 134 | 135 | export class HackWorkspaceSymbolProvider 136 | implements vscode.WorkspaceSymbolProvider 137 | { 138 | public provideWorkspaceSymbols( 139 | query: string, 140 | ): vscode.ProviderResult { 141 | return hh_client.search(query).then((searchResult) => { 142 | const symbols: vscode.SymbolInformation[] = []; 143 | searchResult.forEach((element) => { 144 | const name = element.name.split("\\").pop() || ""; 145 | let desc = element.desc; 146 | if (desc.includes(" in ")) { 147 | desc = desc.slice(0, element.desc.indexOf(" in ")); 148 | } 149 | const kind = getSymbolKind(desc); 150 | const uri: vscode.Uri = utils.mapToWorkspaceUri(element.filename); 151 | const container = 152 | element.scope || 153 | (element.name.includes("\\") 154 | ? element.name.slice(0, element.name.lastIndexOf("\\")) 155 | : undefined); 156 | const range = getRange( 157 | element.line, 158 | element.line, 159 | element.char_start, 160 | element.char_end, 161 | ); 162 | symbols.push( 163 | new vscode.SymbolInformation(name, kind, range, uri, container), 164 | ); 165 | }); 166 | return symbols; 167 | }); 168 | } 169 | } 170 | 171 | export class HackDocumentHighlightProvider 172 | implements vscode.DocumentHighlightProvider 173 | { 174 | public provideDocumentHighlights( 175 | document: vscode.TextDocument, 176 | position: vscode.Position, 177 | ): vscode.ProviderResult { 178 | return hh_client 179 | .ideHighlightRefs( 180 | document.getText(), 181 | position.line + 1, 182 | position.character + 1, 183 | ) 184 | .then((highlightResult) => { 185 | const highlights: vscode.DocumentHighlight[] = []; 186 | highlightResult.forEach((element) => { 187 | const line: number = element.line - 1; 188 | const charStart: number = element.char_start - 1; 189 | const charEnd: number = element.char_end; 190 | highlights.push( 191 | new vscode.DocumentHighlight( 192 | new vscode.Range( 193 | new vscode.Position(line, charStart), 194 | new vscode.Position(line, charEnd), 195 | ), 196 | vscode.DocumentHighlightKind.Text, 197 | ), 198 | ); 199 | }); 200 | return highlights; 201 | }); 202 | } 203 | } 204 | 205 | export class HackCompletionItemProvider 206 | implements vscode.CompletionItemProvider 207 | { 208 | public provideCompletionItems( 209 | document: vscode.TextDocument, 210 | position: vscode.Position, 211 | ): vscode.ProviderResult { 212 | return hh_client 213 | .autoComplete(document.getText(), document.offsetAt(position)) 214 | .then((completionResult) => { 215 | const completionItems: vscode.CompletionItem[] = []; 216 | completionResult.forEach((element) => { 217 | let label: string = element.name.split("\\").pop() || ""; 218 | let labelType: string = element.type; 219 | let kind = vscode.CompletionItemKind.Class; 220 | if (label.startsWith("$")) { 221 | label = label.slice(1); 222 | kind = vscode.CompletionItemKind.Variable; 223 | labelType = labelType.split("\\").pop() || ""; 224 | } else if (labelType.startsWith("(function")) { 225 | // If the name and return type matches then it is a constructor 226 | if (element.name === element.func_details.return_type) { 227 | kind = vscode.CompletionItemKind.Constructor; 228 | } else { 229 | kind = vscode.CompletionItemKind.Method; 230 | } 231 | const typeSplit = labelType 232 | .slice(1, labelType.length - 1) 233 | .split(":"); 234 | labelType = `${typeSplit[0]}: ${typeSplit[1].split("\\").pop()}`; 235 | } else if (labelType === "class") { 236 | kind = vscode.CompletionItemKind.Class; 237 | } 238 | const completionItem = new vscode.CompletionItem(label, kind); 239 | completionItem.detail = labelType; 240 | completionItems.push(completionItem); 241 | }); 242 | return completionItems; 243 | }); 244 | } 245 | } 246 | 247 | export class HackDocumentFormattingEditProvider 248 | implements vscode.DocumentFormattingEditProvider 249 | { 250 | public provideDocumentFormattingEdits( 251 | document: vscode.TextDocument, 252 | ): vscode.ProviderResult { 253 | const text: string = document.getText(); 254 | return hh_client.format(text, 0, text.length).then((formatResult) => { 255 | if (formatResult.internal_error || formatResult.error_message) { 256 | return; 257 | } 258 | const textEdit = vscode.TextEdit.replace( 259 | new vscode.Range( 260 | document.positionAt(0), 261 | document.positionAt(text.length), 262 | ), 263 | formatResult.result, 264 | ); 265 | return [textEdit]; 266 | }); 267 | } 268 | } 269 | 270 | export class HackReferenceProvider implements vscode.ReferenceProvider { 271 | public provideReferences( 272 | document: vscode.TextDocument, 273 | position: vscode.Position, 274 | ): vscode.ProviderResult { 275 | const text = document.getText(); 276 | return hh_client 277 | .ideFindRefs(text, position.line + 1, position.character + 1) 278 | .then((foundRefs) => { 279 | return hh_client 280 | .ideHighlightRefs(text, position.line + 1, position.character + 1) 281 | .then((highlightRefs) => { 282 | const locations: vscode.Location[] = []; 283 | foundRefs.forEach((ref) => { 284 | const location = new vscode.Location( 285 | utils.mapToWorkspaceUri(ref.filename), 286 | new vscode.Range( 287 | new vscode.Position(ref.line - 1, ref.char_start - 1), 288 | new vscode.Position(ref.line - 1, ref.char_end), 289 | ), 290 | ); 291 | locations.push(location); 292 | }); 293 | highlightRefs.forEach((ref) => { 294 | const location = new vscode.Location( 295 | vscode.Uri.file(document.fileName), 296 | new vscode.Range( 297 | new vscode.Position(ref.line - 1, ref.char_start - 1), 298 | new vscode.Position(ref.line - 1, ref.char_end), 299 | ), 300 | ); 301 | locations.push(location); 302 | }); 303 | return locations; 304 | }); 305 | }); 306 | } 307 | } 308 | 309 | export class HackDefinitionProvider implements vscode.DefinitionProvider { 310 | public provideDefinition( 311 | document: vscode.TextDocument, 312 | position: vscode.Position, 313 | ): vscode.ProviderResult { 314 | const text = document.getText(); 315 | return hh_client 316 | .ideGetDefinition(text, position.line + 1, position.character + 1) 317 | .then((foundDefinition) => { 318 | const definition: vscode.Location[] = []; 319 | foundDefinition.forEach((element) => { 320 | if (element.definition_pos) { 321 | const location: vscode.Location = new vscode.Location( 322 | element.definition_pos.filename 323 | ? utils.mapToWorkspaceUri(element.definition_pos.filename) 324 | : document.uri, 325 | new vscode.Range( 326 | new vscode.Position( 327 | element.definition_pos.line - 1, 328 | element.definition_pos.char_start - 1, 329 | ), 330 | new vscode.Position( 331 | element.definition_pos.line - 1, 332 | element.definition_pos.char_end, 333 | ), 334 | ), 335 | ); 336 | definition.push(location); 337 | } 338 | }); 339 | return definition; 340 | }); 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /src/proxy.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file hh_client proxy 3 | */ 4 | 5 | import * as ps from "child_process"; 6 | import * as config from "./Config"; 7 | import * as remote from "./remote"; 8 | import * as hack from "./types/hack"; 9 | 10 | export async function version(): Promise { 11 | return run(["--version"]); 12 | } 13 | 14 | /** 15 | * Hack client hangs if executed in lsp mode before running it standalone. 16 | */ 17 | export async function start(): Promise { 18 | return run([]); 19 | } 20 | 21 | export async function check(): Promise { 22 | return run(["check"]); 23 | } 24 | 25 | export async function typeAtPos( 26 | fileName: string, 27 | line: number, 28 | character: number, 29 | ): Promise { 30 | const arg: string = `${fileName}:${line}:${character}`; 31 | const args: string[] = ["--type-at-pos", arg]; 32 | const typeAtPosResponse: hack.TypeAtPosResponse = await run(args); 33 | 34 | if ( 35 | !typeAtPosResponse || 36 | !typeAtPosResponse.type || 37 | typeAtPosResponse.type === "(unknown)" || 38 | typeAtPosResponse.type === "_" || 39 | typeAtPosResponse.type === "noreturn" 40 | ) { 41 | return undefined; 42 | } 43 | return typeAtPosResponse.type; 44 | } 45 | 46 | export async function outline(text: string): Promise { 47 | return run(["--outline"], text); 48 | } 49 | 50 | export async function search(query: string): Promise { 51 | return run(["--search", query]); 52 | } 53 | 54 | export async function ideFindRefs( 55 | text: string, 56 | line: number, 57 | character: number, 58 | ): Promise { 59 | return run(["--ide-find-refs", `${line}:${character}`], text); 60 | } 61 | 62 | export async function ideHighlightRefs( 63 | text: string, 64 | line: number, 65 | character: number, 66 | ): Promise { 67 | return run(["--ide-highlight-refs", `${line}:${character}`], text); 68 | } 69 | 70 | export async function ideGetDefinition( 71 | text: string, 72 | line: number, 73 | character: number, 74 | ): Promise { 75 | return run(["--ide-get-definition", `${line}:${character}`], text); 76 | } 77 | 78 | export async function autoComplete( 79 | text: string, 80 | position: number, 81 | ): Promise { 82 | // Insert hh_client autocomplete token at cursor position. 83 | const autoTok: string = "AUTO332"; 84 | const input = [text.slice(0, position), autoTok, text.slice(position)].join( 85 | "", 86 | ); 87 | return run(["--auto-complete"], input); 88 | } 89 | 90 | export async function format( 91 | text: string, 92 | startPos: number, 93 | endPos: number, 94 | ): Promise { 95 | // `endPos` is incremented to stop `hh_client --format` from removing the 96 | // final character when there is no newline at the end of the file. 97 | // 98 | // This appears to be a bug in `hh_client --format`. 99 | return run(["--format", startPos.toString(), (endPos + 1).toString()], text); 100 | } 101 | 102 | async function run(extraArgs: string[], stdin?: string): Promise { 103 | return new Promise((resolve, _) => { 104 | const workspacePath = 105 | config.remoteEnabled && config.remoteWorkspacePath 106 | ? config.remoteWorkspacePath 107 | : config.localWorkspacePath; 108 | const command = remote.getCommand(config.clientPath); 109 | const args = remote.getArgs(config.clientPath, [ 110 | ...extraArgs, 111 | "--json", 112 | "--from", 113 | "vscode-hack", 114 | workspacePath, 115 | ]); 116 | const p = ps.execFile( 117 | command, 118 | args, 119 | { maxBuffer: 1024 * 1024 }, 120 | (err: any, stdout, stderr) => { 121 | if (err !== null && err.code !== 0 && err.code !== 2) { 122 | // any hh_client failure other than typecheck errors 123 | console.error(`Hack: hh_client execution error: ${err}`); 124 | resolve(null); 125 | } 126 | if (!stdout) { 127 | // all hh_client --check output goes to stderr by default 128 | stdout = stderr; 129 | } 130 | try { 131 | const output = JSON.parse(stdout); 132 | resolve(output); 133 | } catch (parseErr) { 134 | console.error(`Hack: hh_client output error: ${parseErr}`); 135 | resolve(null); 136 | } 137 | }, 138 | ); 139 | if (stdin && p && p.stdin) { 140 | p.stdin.write(stdin); 141 | p.stdin.end(); 142 | } 143 | }); 144 | } 145 | -------------------------------------------------------------------------------- /src/remote.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Helpers for remote connections to hh_client and hhast-lint 3 | */ 4 | 5 | import * as config from "./Config"; 6 | 7 | export function getCommand(command: string): string { 8 | if (!config.remoteEnabled) { 9 | return command; 10 | } 11 | 12 | if (config.remoteType === "ssh") { 13 | return "ssh"; 14 | } else if (config.remoteType === "docker") { 15 | return "docker"; 16 | } else { 17 | // Error for unrecognized remote type 18 | return command; 19 | } 20 | } 21 | 22 | export function getArgs(command: string, args: string[]): string[] { 23 | if (!config.remoteEnabled) { 24 | return args; 25 | } 26 | 27 | if (config.remoteType === "ssh") { 28 | return [...config.sshArgs, config.sshHost, command, ...args]; 29 | } else if (config.remoteType === "docker") { 30 | return ["exec", "-i", config.dockerContainerName, command, ...args]; 31 | } else { 32 | // Error for unrecognized remote type 33 | return args; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | 3 | import { runTests } from "@vscode/test-electron"; 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, "../../"); 10 | 11 | // The path to test runner 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, "./suite/index"); 14 | 15 | // Download VS Code, unzip it and run the integration test 16 | await runTests({ extensionDevelopmentPath, extensionTestsPath }); 17 | } catch (err) { 18 | console.error("Failed to run tests"); 19 | process.exit(1); 20 | } 21 | } 22 | 23 | main(); 24 | -------------------------------------------------------------------------------- /src/test/suite/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from "assert"; 2 | 3 | // You can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | import * as vscode from "vscode"; 6 | // import * as myExtension from '../../extension'; 7 | 8 | suite("Extension Test Suite", () => { 9 | vscode.window.showInformationMessage("Start all tests."); 10 | 11 | test("Sample test", () => { 12 | assert.strictEqual(-1, [1, 2, 3].indexOf(5)); 13 | assert.strictEqual(-1, [1, 2, 3].indexOf(0)); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/test/suite/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as Mocha from "mocha"; 3 | import * as glob from "glob"; 4 | 5 | export function run(): Promise { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: "tdd", 9 | color: true, 10 | }); 11 | 12 | const testsRoot = path.resolve(__dirname, ".."); 13 | 14 | return new Promise((c, e) => { 15 | glob("**/**.test.js", { cwd: testsRoot }, (err, files) => { 16 | if (err) { 17 | return e(err); 18 | } 19 | 20 | // Add files to the test suite 21 | files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))); 22 | 23 | try { 24 | // Run the mocha test 25 | mocha.run((failures) => { 26 | if (failures > 0) { 27 | e(new Error(`${failures} tests failed.`)); 28 | } else { 29 | c(); 30 | } 31 | }); 32 | } catch (err) { 33 | console.error(err); 34 | e(err); 35 | } 36 | }); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /src/types/hack.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Types for hh_client responses 3 | */ 4 | 5 | type Version = { 6 | commit: string; 7 | commit_time: number; 8 | api_version: number; 9 | }; 10 | 11 | type Position = { 12 | filename: string; 13 | line: number; 14 | char_start: number; 15 | char_end: number; 16 | }; 17 | 18 | type Span = { 19 | filename: string; 20 | line_start: number; 21 | char_start: number; 22 | line_end: number; 23 | char_end: number; 24 | }; 25 | 26 | type CheckResponse = { 27 | passed: boolean; 28 | errors: { 29 | message: { 30 | descr: string; 31 | path: string; 32 | line: number; 33 | start: number; 34 | end: number; 35 | code: number; 36 | }[]; 37 | }[]; 38 | }; 39 | 40 | export type OutlineResponse = { 41 | name: string; 42 | kind: string; 43 | id: string; 44 | position: Position; 45 | span: Span; 46 | children: OutlineResponse[]; 47 | }; 48 | 49 | type SearchResponse = { 50 | name: string; 51 | filename: string; 52 | desc: string; 53 | line: number; 54 | char_start: number; 55 | char_end: number; 56 | scope: string; 57 | }[]; 58 | 59 | type IdeFindRefsResponse = { 60 | name: string; 61 | filename: string; 62 | line: number; 63 | char_start: number; 64 | char_end: number; 65 | }[]; 66 | 67 | type IdeHighlightRefsResponse = { 68 | line: number; 69 | char_start: number; 70 | char_end: number; 71 | }[]; 72 | 73 | type IdeGetDefinitionResponse = { 74 | name: string; 75 | result_type: string; 76 | pos: Position; 77 | definition_pos: Position; 78 | definition_span: Span; 79 | definition_id: number; 80 | }[]; 81 | 82 | type AutoCompleteResponse = { 83 | name: string; 84 | type: string; 85 | pos: Position; 86 | func_details: { 87 | min_arity: number; 88 | return_type: string; 89 | params: { 90 | name: string; 91 | type: string; 92 | variadic: boolean; 93 | }[]; 94 | }; 95 | }[]; 96 | 97 | type FormatResponse = { 98 | result: string; 99 | error_message: string; 100 | internal_error: boolean; 101 | }; 102 | 103 | type TypeAtPosResponse = { 104 | type: string; 105 | }; 106 | -------------------------------------------------------------------------------- /src/types/lsp.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Types for custom LSP extensions 3 | */ 4 | 5 | import { Range } from "vscode"; 6 | 7 | type TypeCoverageResponse = { 8 | coveredPercent: number; 9 | uncoveredRanges: { 10 | message: string; 11 | range: Range; 12 | }[]; 13 | defaultMessage: string; 14 | }; 15 | 16 | type ShowStatusRequest = { 17 | shortMessage?: string; 18 | message?: string; 19 | type: number; 20 | }; 21 | -------------------------------------------------------------------------------- /syntaxes/README.md: -------------------------------------------------------------------------------- 1 | # Syntax Generation 2 | 3 | This extension uses the Hack language grammar defined in the [atom-ide-hack project](https://github.com/hhvm/atom-ide-hack). 4 | 5 | The source file is located at [https://github.com/hhvm/atom-ide-hack/blob/master/grammars/hack.cson](https://github.com/hhvm/atom-ide-hack/blob/master/grammars/hack.cson). 6 | 7 | Since it is in CSON format, it has to be converted to JSON before use here. A converter is included as a npm dev dependency, and can be run by: 8 | 9 | ```bash 10 | $ ./node_modules/cson/bin/cson2json syntaxes/hack.cson > syntaxes/hack.json 11 | ``` 12 | -------------------------------------------------------------------------------- /syntaxes/codeblock.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileTypes": [], 3 | "injectionSelector": "L:markup.fenced_code.block.markdown", 4 | "patterns": [ 5 | { 6 | "include": "#hack-code-block" 7 | } 8 | ], 9 | "repository": { 10 | "hack-code-block": { 11 | "begin": "hack", 12 | "end": "(^|\\G)(?=\\s*[`~]{3,}\\s*$)", 13 | "contentName": "meta.embedded.block.hack", 14 | "patterns": [ 15 | { 16 | "include": "source.hack" 17 | } 18 | ] 19 | } 20 | }, 21 | "scopeName": "markdown.hack.codeblock" 22 | } 23 | -------------------------------------------------------------------------------- /syntaxes/test/.hhconfig: -------------------------------------------------------------------------------- 1 | # namespace aliasing 2 | auto_namespace_map = {"Dict": "HH\\Lib\\Dict", "Vec": "HH\\Lib\\Vec", "Keyset": "HH\\Lib\\Keyset", "C": "HH\\Lib\\C", "Str": "HH\\Lib\\Str", "PHP": "HH\\Lib\\PHP", "Math": "HH\\Lib\\Math", "PseudoRandom": "HH\\Lib\\PseudoRandom", "SecureRandom": "HH\\Lib\\SecureRandom", "Rx": "HH\\Rx", "Regex": "HH\\Lib\\Regex"} 3 | 4 | -------------------------------------------------------------------------------- /syntaxes/test/README.md: -------------------------------------------------------------------------------- 1 | # Syntax coloring test 2 | 3 | This folder contains interesting examples of hack code that can be used to demonstrate various syntax highlighting issues. 4 | 5 | To report a bug for syntax highlighting, please submit a file that demonstrates a snippet of code that is colored incorrectly. 6 | 7 | To test updates to syntax coloring, open this folder and verify the examples. 8 | 9 | ```hack 10 | // This embedded code block within the markdown should also be properly highlighted in the manner of a hack file. 11 | function f() { 12 | return; 13 | } 14 | ``` 15 | -------------------------------------------------------------------------------- /syntaxes/test/abstract_final.hack: -------------------------------------------------------------------------------- 1 | abstract final class MyFoo {} 2 | 3 | class MyBar {} 4 | -------------------------------------------------------------------------------- /syntaxes/test/async.hack: -------------------------------------------------------------------------------- 1 | async function gen_foo(): Awaitable { 2 | return 1; 3 | } 4 | 5 | async function gen_bar(): Awaitable { 6 | concurrent { 7 | $x = await gen_foo(1); 8 | $y = await gen_foo(2); 9 | } 10 | } -------------------------------------------------------------------------------- /syntaxes/test/basic_types.hack: -------------------------------------------------------------------------------- 1 | // All of these type names should be highlighted the same way. 2 | function foo(num $x, string $s, int $s, Thing $x): void { 3 | if ($x is Foo) { 4 | $x as Thing; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /syntaxes/test/bools.hack: -------------------------------------------------------------------------------- 1 | 2 | function booleans(): void { 3 | // normal boolean literals 4 | $x = true; 5 | $y = false; 6 | 7 | // booleans are case insensitive 8 | $x = True; 9 | $y = FALSE; 10 | 11 | // yes/no are not booleans. 12 | $x = yes; 13 | $y = no; 14 | 15 | // logical operators. 16 | $x = !true; 17 | $y = $x && ($x || true); 18 | } 19 | -------------------------------------------------------------------------------- /syntaxes/test/collections.hack: -------------------------------------------------------------------------------- 1 | 2 | function create_collections(): void { 3 | $v = Vector {}; 4 | $v2 = ImmVector {}; 5 | 6 | $m = Map { 'x' => 1 }; 7 | $m2 = ImmMap { 'x' => 1 }; 8 | 9 | $m = Set {}; 10 | $m2 = ImmSet {}; 11 | 12 | $p = Pair { 1, 2 }; 13 | } 14 | -------------------------------------------------------------------------------- /syntaxes/test/default_param_new_line.hack: -------------------------------------------------------------------------------- 1 | class Foo { 2 | public static function bar( 3 | mixed $arg = 4 | 'baz(biz //")' 5 | ): string { 6 | return "foo"; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /syntaxes/test/double_greater_than.hack: -------------------------------------------------------------------------------- 1 | // In this test, there are two >> symbols next to each other. 2 | // They should not break the overall flow of syntax coloring. 3 | // Everything after shape(...)>> should be colored correctly. 4 | 5 | protected static function initializeCoValidators( 6 | ): vec> { 7 | return vec[]; 8 | } -------------------------------------------------------------------------------- /syntaxes/test/enum_classes.hack: -------------------------------------------------------------------------------- 1 | enum class SimpleEnumClass: mixed { 2 | int X = 42; 3 | string S = 'foo'; 4 | } 5 | 6 | enum class WithInheritance: IFoo extends BaseFoo { 7 | } 8 | 9 | 10 | -------------------------------------------------------------------------------- /syntaxes/test/enums.hack: -------------------------------------------------------------------------------- 1 | enum SimpleEnum: int as arraykey { 2 | X = 1; 3 | Y = 2; 4 | Z = 3; 5 | ENUM_FOO = 1; 6 | ENUM_CLASS_BAR = 2; 7 | } 8 | 9 | enum ComplicatedEnum: classname as classname {} 10 | -------------------------------------------------------------------------------- /syntaxes/test/generic_shape_type.hack: -------------------------------------------------------------------------------- 1 | // This function should correctly highlight elements after the "shape(...)" syntax 2 | 3 | function trace_shape_scalars_as_span_tags(SpanRef $span, shape(...) $shape, vec(arraykey) $v) { 4 | foreach (Shapes::toDict($shape) as $key => $value) { 5 | if ($value is int) { 6 | t($span, $key_prefix.(string)$key, $value); 7 | } else if ($value is float) { 8 | t($span, $key_prefix.(string)$key, $value); 9 | } else if ($value is bool) { 10 | t($span, $key_prefix.(string)$key, $value); 11 | } else if ($value is string) { 12 | t($span, $key_prefix.(string)$key, $value); 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /syntaxes/test/interpolation.hack: -------------------------------------------------------------------------------- 1 | // See https://www.php.net/manual/en/language.types.string.php#language.types.string.parsing 2 | function double_quoted(): void { 3 | // Escape characters. 4 | $x = "world \n \t \\"; 5 | 6 | // Octal/hex. 7 | $x = "world \07 \xFF" 8 | 9 | // Simple interpolation. 10 | $y = "hello $x"; 11 | 12 | // We can do a single array access, but not nested. 13 | $y = "hello $x[0]"; 14 | $y = "hello $x[0][1]"; // literal "[1]" 15 | 16 | // Similarly with properties, we can only access the first. 17 | $y = "hello $x->foo"; 18 | $y = "hello $x->foo->bar"; // literal "->bar" 19 | 20 | // In {} we can use more complex expressions. 21 | $y = "hello {$x->y->z[0][1] + 2}"; 22 | 23 | // Just accessing $x here due to the space. 24 | $y = "hello { $x + 1}"; 25 | } 26 | 27 | function single_quoted(): void { 28 | // This is a literal \ and n. 29 | $x = 'foo\n bar'; 30 | 31 | // No interpolation here. 32 | $x = 'foo $x'; 33 | } 34 | 35 | function regex(): void { 36 | // In a regular expression, we want to highlight metacharacters. 37 | $x = re"/* foo */"; 38 | $x = re'/* foo */'; 39 | 40 | // Don't treat plain strings with / as regexps. 41 | $x = "/* foo */"; 42 | $x = '/* foo */'; 43 | } -------------------------------------------------------------------------------- /syntaxes/test/modules.hack: -------------------------------------------------------------------------------- 1 | module foo.bar.baz; 2 | 3 | new module foo.bar.baz { 4 | } 5 | 6 | module newtype x = y; 7 | 8 | internal interface foo { 9 | } 10 | 11 | public interface foo { 12 | } 13 | 14 | interface foo { 15 | } 16 | 17 | internal enum foo { 18 | } 19 | 20 | public enum foo { 21 | } 22 | 23 | enum foo { 24 | } 25 | 26 | internal enum class foo { 27 | } 28 | 29 | public enum class foo { 30 | } 31 | 32 | enum class foo { 33 | } 34 | 35 | internal trait foo { 36 | internal int $x; 37 | internal function y(): void { 38 | } 39 | } 40 | 41 | public trait foo { 42 | } 43 | 44 | trait foo { 45 | } 46 | 47 | internal class foo { 48 | internal int $x; 49 | internal function y(): void { 50 | } 51 | } 52 | 53 | public class foo { 54 | } 55 | 56 | class foo { 57 | } 58 | 59 | internal function foo(): void { 60 | } 61 | 62 | public function foo(): void { 63 | } 64 | 65 | function foo(): void { 66 | } -------------------------------------------------------------------------------- /syntaxes/test/namespaces.hack: -------------------------------------------------------------------------------- 1 | namespace foo { 2 | function one(): void {} 3 | } 4 | 5 | namespace { 6 | function two(): void {} 7 | } 8 | -------------------------------------------------------------------------------- /syntaxes/test/order_of_attributes.hack: -------------------------------------------------------------------------------- 1 | // Syntax highlighting of attributes should be correct. 2 | // In this example both "Lossy" and "Safe" should show 3 | // as the same color regardless of order. 4 | 5 | namespace Cast; 6 | <> 7 | function num_int_round(num $num): int { 8 | return $num is int ? $num : float_int_round($num as float); 9 | } 10 | 11 | <> 12 | function num_int_round_2(num $num): int { 13 | return $num is int ? $num : float_int_round($num as float); 14 | } -------------------------------------------------------------------------------- /syntaxes/test/shape_constructor.hack: -------------------------------------------------------------------------------- 1 | // The "shape()" constructor below should highlight as yellow 2 | 3 | function foo(int $arg): shape(...){ 4 | return shape(); 5 | } -------------------------------------------------------------------------------- /syntaxes/test/unpaired_apostrophe.hack: -------------------------------------------------------------------------------- 1 | // This file has an error (unpaired quote). Everything after line 6 2 | // should be colored as part of the string. 3 | 4 | <<__EntryPoint>> 5 | function foo(): void { 6 | $x = 'SELECT (')'); 7 | echo $x; 8 | } 9 | -------------------------------------------------------------------------------- /syntaxes/test/xhp_with_apostrophe.hack: -------------------------------------------------------------------------------- 1 | // This function returns an XHP snippet with embedded text 2 | // that has some unpaired symbols. 3 | 4 | public static function getMetric(): :foo { 5 | return 6 | /* The text within this XHP snippet should be colored white */ 7 | 8 | Testing embedded text with unpaired symbols that can cause breaks: 9 | ( 10 | ' 11 | " 12 | \ 13 | } 14 | < 15 | > 16 | ; 17 | /* After the text, normal syntax highlighting resumes */ 18 | } 19 | 20 | public function anotherFunction(): int { 21 | return 42; 22 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2020", 5 | "outDir": "out", 6 | "lib": ["ES2020"], 7 | "sourceMap": true, 8 | "rootDir": "src", 9 | "strict": true /* enable all strict type-checking options */ 10 | /* Additional Checks */ 11 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 12 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 13 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 14 | }, 15 | "exclude": ["node_modules", ".vscode-test"] 16 | } 17 | --------------------------------------------------------------------------------