├── .devcontainer.json ├── .github ├── dependabot.yml └── workflows │ └── workflow.yaml ├── .gitignore ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── data ├── grammar │ ├── cabal.json │ └── haskell.json ├── image │ └── icon.png ├── language │ ├── cabal.json │ └── haskell.json └── snippet │ ├── cabal.json │ └── haskell.json ├── package.json ├── source ├── client.ts └── type │ ├── CabalFormatterMode.ts │ ├── HaskellFormatterMode.ts │ ├── HaskellLinterMode.ts │ ├── Idea.ts │ ├── IdeaSeverity.ts │ ├── Interpreter.ts │ ├── InterpreterMode.ts │ ├── Key.ts │ ├── LanguageId.ts │ ├── NewMessage.ts │ ├── NewMessageLocation.ts │ ├── NewMessageReason.ts │ ├── NewMessageSeverity.ts │ ├── NewMessageSpan.ts │ ├── OldMessage.ts │ ├── OldMessageReason.ts │ ├── OldMessageSeverity.ts │ ├── OldMessageSpan.ts │ └── Template.ts └── tsconfig.json /.devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "containerUser": "node", 3 | "image": "mcr.microsoft.com/devcontainers/javascript-node:20" 4 | } 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | updates: 2 | - directory: / 3 | package-ecosystem: github-actions 4 | schedule: 5 | interval: weekly 6 | - directory: / 7 | package-ecosystem: npm 8 | schedule: 9 | interval: weekly 10 | version: 2 11 | -------------------------------------------------------------------------------- /.github/workflows/workflow.yaml: -------------------------------------------------------------------------------- 1 | { 2 | "jobs": { 3 | "build": { 4 | "runs-on": "ubuntu-latest", 5 | "steps": [ 6 | { 7 | "uses": "actions/checkout@v4" 8 | }, 9 | { 10 | "uses": "actions/setup-node@v4", 11 | "with": { 12 | "node-version": "20" 13 | } 14 | }, 15 | { 16 | "run": "npm install" 17 | }, 18 | { 19 | "run": "npx vsce package --out purple-yolk.vsix" 20 | }, 21 | { 22 | "uses": "actions/upload-artifact@v4", 23 | "with": { 24 | "name": "purple-yolk-${{ github.sha }}.vsix", 25 | "path": "purple-yolk.vsix" 26 | } 27 | }, 28 | { 29 | "env": { 30 | "GITHUB_TOKEN": "${{ secrets.GITHUB_TOKEN }}" 31 | }, 32 | "if": "github.event_name == 'release'", 33 | "uses": "svenstaro/upload-release-action@v2", 34 | "with": { 35 | "asset_name": "purple-yolk-${{ github.event.release.tag_name }}.vsix", 36 | "file": "purple-yolk.vsix" 37 | } 38 | }, 39 | { 40 | "if": "github.event_name == 'release'", 41 | "run": "npx vsce publish --packagePath purple-yolk.vsix --pat \"${{ secrets.AZURE_PERSONAL_ACCESS_TOKEN }}\"" 42 | }, 43 | { 44 | "if": "github.event_name == 'release'", 45 | "run": "npx ovsx publish --packagePath purple-yolk.vsix --pat \"${{ secrets.OVSX_PAT }}\"" 46 | } 47 | ] 48 | } 49 | }, 50 | "name": "Workflow", 51 | "on": { 52 | "pull_request": { 53 | "branches": [ 54 | "main" 55 | ] 56 | }, 57 | "push": { 58 | "branches": [ 59 | "main" 60 | ] 61 | }, 62 | "release": { 63 | "types": [ 64 | "created" 65 | ] 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # https://git-scm.com/docs/gitignore 2 | /.vscode/ 3 | /dist/ 4 | /node_modules/ 5 | /package-lock.json 6 | /purple-yolk-*.vsix 7 | /yarn.lock 8 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | # https://code.visualstudio.com/api/working-with-extensions/publishing-extension#using-.vscodeignore 2 | **/* 3 | 4 | !data/ 5 | !dist/ 6 | !CHANGELOG.md 7 | !LICENSE.txt 8 | !package.json 9 | !README.md 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change log 2 | 3 | Purple Yolk uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 4 | 5 | ## 2025-03-18: 1.1.0 6 | 7 | - Added ability to use Stack for non-project workspaces. 8 | 9 | ## 2025-03-17: 1.0.10 10 | 11 | - Also publish extension to Open VSX Registry. 12 | 13 | ## 2025-03-08: 1.0.9 14 | 15 | - Fixed handling of JSON diagnostics from newer versions of GHC using `-fdiagnostics-as-json`. 16 | 17 | ## 2025-03-02: 1.0.8 18 | 19 | - Fixed a bug that caused diagnostics to not be cleared when using Cabal's `--enable-multi-repl` flag. 20 | 21 | ## 2024-06-22: 1.0.7 22 | 23 | - Fixed a bug that prevented Purple Yolk from working on Windows due to problems with getting the current directory. 24 | - Fixed CI to run on pull requests. 25 | - Bumped esbuild from 0.20.2 to 0.21.2. 26 | 27 | ## 2024-05-09: 1.0.6 28 | 29 | - Fixed a bug that prevented Purple Yolk from working with individual Haskell files through GHCi. 30 | 31 | ## 2024-05-06: 1.0.5 32 | 33 | - Fixed a bug that prevented problems from being cleared when using Stack. 34 | 35 | ## 2024-05-03: 1.0.4 36 | 37 | - Fixed another bug that would sometimes prevent the Haskell interpreter from starting. 38 | 39 | ## 2024-05-02: 1.0.3 40 | 41 | - Fixed a bug that would sometimes prevent the Haskell interpreter from starting. 42 | - Fixed diagnostic collection names. Now they're `ghc` and `hlint` rather than `purple-yolk`. 43 | - Fixed a bug that would sometimes get the status bar item stuck displaying an error icon. 44 | 45 | ## 2024-04-22: 1.0.2 46 | 47 | - Fixed diagnostic codes (like `unused-top-binds`) with GHC >= 9.4. 48 | - Added tags (like unused or deprecated) to diagnostics. 49 | 50 | ## 2024-02-20: 1.0.1 51 | 52 | - Updated documentation. 53 | 54 | ## 2024-02-18: 1.0.0 55 | 56 | - Enabled HLint on save by default. 57 | 58 | ## 2024-02-18: 0.13.0 59 | 60 | - Added Gild as a formatter for Cabal files. 61 | 62 | ## 2023-09-19: 0.12.0 63 | 64 | - Changed "discover" configuration to prefer custom commands when they are present. 65 | 66 | ## 2023-09-09: 0.11.1 67 | 68 | - Improved automatic discovery of Haskell formatter. 69 | 70 | ## 2023-09-06: 0.11.0 71 | 72 | - Improved configuration to be more automatic. 73 | 74 | ## 2023-08-28: 0.10.2 75 | 76 | - Fixed a bug that caused diagnostics from Stack to be reported at the wrong location. 77 | 78 | ## 2023-06-27: 0.10.1 79 | 80 | - No changes. 81 | 82 | ## 2023-05-24: 0.10.0 83 | 84 | - Added template variable expansion to commands. You can use `${file}` as the path to the current file. 85 | 86 | ## 2023-01-25: 0.9.2 87 | 88 | - Changed the default Cabal file formatter to allow formatting more files, like `cabal.project`. 89 | 90 | ## 2023-01-14: 0.9.1 91 | 92 | - Fixed a bug that assigned the wrong severity to diagnostics when using GHC >= 9.4. 93 | - Fixed a bug that attempted to lint non-Haskell documents in certain situations. 94 | 95 | ## 2022-11-01: 0.9.0 96 | 97 | - Added the ability to format Cabal files. 98 | 99 | ## 2022-08-02: 0.8.2 100 | 101 | - Fixed language status item details. 102 | 103 | ## 2022-08-02: 0.8.1 104 | 105 | - Changed interpreter to avoid reloading while a reload is ongoing. 106 | - Added workspace diagnostics to interpreter. 107 | - Changed interpreter to clear workspace diagnostics on reload. 108 | - Changed linter to report all diagnostics as "information". 109 | - Fixed language status item severity. 110 | 111 | ## 2022-08-02: 0.8.0 112 | 113 | - Rewrote everything: . 114 | - No more LSP server; now just a VSCode client. 115 | - Status reported through a language status bar item. 116 | - Changed commands: 117 | - `purple-yolk.lintFile` is now `purple-yolk.haskell.lint` 118 | - `purple-yolk.restart` is now `purple-yolk.haskell.interpret` 119 | - `purple-yolk.showOutput` is now `purple-yolk.output.show` 120 | - Changed configuration properties: 121 | - `purple-yolk.brittany.command` is now `purple-yolk.haskell.formatter.command` 122 | - `purple-yolk.ghci.command` is now `purple-yolk.haskell.interpreter.command` 123 | - `purple-yolk.hlint.command` is now `purple-yolk.haskell.linter.command` 124 | - `purple-yolk.hlint.onSave` is now `purple-yolk.haskell.linter.onSave` 125 | 126 | ## 2022-07-27: 0.7.2 127 | 128 | - Added syntax highlighting for `cabal.project`, `cabal.project.local`, and 129 | `cabal.project.freeze` files. 130 | - Improved how elapsed times are logged. 131 | - Updated dependencies. 132 | 133 | ## 2022-06-19: 0.7.1 134 | 135 | - Fixed a bug that prevented the client from starting. 136 | 137 | ## 2022-06-18: 0.7.0 138 | 139 | - Switched license from ISC to MIT. 140 | - Prevented Brittany and HLint from crashing the language server. 141 | 142 | ## 2021-02-25: 0.6.1 143 | 144 | - Added syntax highlighting for operators. Thanks Daniel Sokil! 145 | 146 | ## 2021-02-02: 0.6.0 147 | 148 | - Added rudimentary formatting support. 149 | 150 | ## 2021-02-01: 0.5.0 151 | 152 | - Added tags to diagnostics, which label unnecessary and deprecated things. 153 | - Fixed a bug that incorrectly highlighted some operators as comments. 154 | 155 | ## 2021-01-30: 0.4.1 156 | 157 | - No changes. 158 | 159 | ## 2021-01-30: 0.4.0 160 | 161 | - Added the ability to lint on save and enabled it by default. 162 | - Fixed a bug that cleared diagnostics when linting. 163 | 164 | ## 2021-01-14: 0.3.0 165 | 166 | - Added basic linting through the `purple-yolk.lintFile` command. 167 | 168 | ## 2020-12-31: 0.2.3 169 | 170 | - Changed the progress notification to only show after one second. 171 | - Fixed a bug that caused multiple duplicate commands to be queued. 172 | 173 | ## 2020-10-03: 0.2.2 174 | 175 | - Fixed a bug with the default GHCi command. 176 | 177 | ## 2020-09-27: 0.2.1 178 | 179 | - Fixed a bug with the default GHCi command on Windows. 180 | 181 | ## 2020-09-27: 0.2.0 182 | 183 | - Started displaying progress notifications. 184 | - Started reporting import cycles as errors. 185 | 186 | ## 2020-09-18: 0.1.2 187 | 188 | - No changes. 189 | 190 | ## 2020-09-18: 0.1.1 191 | 192 | - No changes. 193 | 194 | ## 2020-09-18: 0.1.0 195 | 196 | - Switched implementation from PureScript to JavaScript. 197 | - Changed namespace for commands and config from `purpleYolk.*` to `purple-yolk.*`. 198 | 199 | ## 2020-05-26: 0.0.11 200 | 201 | - Made it so the status bar item takes you to Purple Yolk's output when you click on it. 202 | 203 | ## 2020-05-26: 0.0.10 204 | 205 | - Fixed how GHCi is managed to avoid crashing when a buffer fills up. 206 | 207 | ## 2020-05-24: 0.0.9 208 | 209 | - Changed how GHCi restarts to make it more reliable. 210 | - Made the default command more compatible with Windows. 211 | 212 | ## 2020-05-22: 0.0.8 213 | 214 | - Changed the GHCi command to allow quoted arguments. This lets you pass multiple arguments through to GHCi, for example with `stack ghci --ghc-options '-O0 -j4'. 215 | - Changed the default GHCi command to build benchmarks and test suites, as well as passing `-fobject-code -j4 -O0` to GHC. 216 | 217 | ## 2020-05-22: 0.0.7 218 | 219 | - Added a status bar item to show progress. This allows you to see what Purple Yolk is working on without checking the output panel. 220 | 221 | ## 2020-05-22: 0.0.6 222 | 223 | - Added the "Purple Yolk: Restart" command to restart GHCi. This is useful when you change something, like a `*.cabal` file, that Purple Yolk doesn't notice. 224 | - Stopped setting `-fdefer-type-errors`, `-fno-code`, and `-j` when starting GHCi. If you want these settings enabled, add them to your GHCi command. 225 | - Change the job queue to avoid enqueueing duplicate jobs. This avoids repeatedly running `:reload` when you save multiple files. 226 | - Changed diagnostics to clear when compiling that file, instead of clearing everything when a reload starts. This prevents some diagnostics from disappearing on reload. 227 | - Changed the default command to set `-ddump-json`. 228 | - Improved startup to more reliably capture warnings and errors. 229 | 230 | ## 2019-12-21: 0.0.5 231 | 232 | - Simplified GHCi command. 233 | 234 | ## 2019-12-20: 0.0.4 235 | 236 | - Rewrote internals to be easier to maintain and extend. 237 | 238 | ## 2019-12-09: 0.0.3 239 | 240 | - No user facing changes. 241 | 242 | ## 2019-12-06: 0.0.2 243 | 244 | - Added the `purpleYolk.ghci.command` configuration option for customizing how to start GHCi. 245 | 246 | ## 2019-12-06: 0.0.1 247 | 248 | - Initially released. 249 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Taylor Fausak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Purple Yolk 2 | 3 | Purple Yolk is an extension for Visual Studio Code that provides a simple IDE 4 | for Haskell projects. 5 | 6 | Purple Yolk is designed to work with Haskell projects rather than individual 7 | Haskell files. That typically means something with a `cabal.project` or 8 | `stack.yaml` file. Although Purple Yolk can work with a single Haskell file, 9 | its utility is somewhat limited. 10 | 11 | At a high level, Purple Yolk launches GHCi for you, reloads it when you make 12 | changes, and displays GHCi's output in VS Code. It is designed to work with any 13 | project and build tool that can provide a REPL. It includes built-in support 14 | for both Cabal and Stack. Other tools are supported via custom configuration. 15 | 16 | ## Features 17 | 18 | These are the features that Purple Yolk provides: 19 | 20 | - Syntax highlighting for both Haskell and Cabal files. 21 | 22 | - Integration with GHCi, displaying GHC's warnings and errors inline and in the 23 | problems tab. This supports both Cabal and Stack for projects as well as GHCi 24 | for individual files. 25 | 26 | - Formatting of Haskell files. Out of the box this supports both Fourmolu and 27 | Ormolu. Additional formatters are supported via custom configuration as long 28 | as the support reading from STDIN and writing to STDOUT. 29 | 30 | - Formatting of Cabal files. Out of the box this supports both `cabal-fmt` and 31 | Gild. Additional formatters are supported via custom configuration as long as 32 | they support reading from STDIN and writing to STDOUT. 33 | 34 | - Linting of Haskell files. Out of the box this supports HLint. Additional 35 | linters are supported via custom configuration, but their output must be the 36 | same as HLint's. 37 | 38 | ## Not Implemented 39 | 40 | These features are not (yet?) implemented. Most of them require a GHCi session 41 | loaded for an individual module rather than the entire project. 42 | 43 | - Automatically suggesting fixes. Consider using [the Haskutil extension][] 44 | along with Purple Yolk. 45 | 46 | [the Haskutil extension]: https://marketplace.visualstudio.com/items?itemName=Edka.haskutil 47 | 48 | - Contextual auto completion. This could be provided with GHCi's `:complete` 49 | command. 50 | 51 | - Documentation on hover. This could be provided with GHCi's `:doc` and/or 52 | `:info` commands. 53 | 54 | - Displaying the type at the cursor. This could be provided with GHCi's `:type` 55 | command. 56 | 57 | - Sending commands directly to the REPL. This could be supported, but extra 58 | safeguards would have to be put in place to avoid breaking the integration. 59 | For example sending a bogus `:load` command could have unintended 60 | consequences. 61 | 62 | - Jump to definition. This is not supported by GHCi. 63 | 64 | - Find usages. This is not supported by GHCi. 65 | 66 | ## Common Problems 67 | 68 | If you delete a file/module, you'll have to restart GHCi for the build to 69 | succeed. Similarly if you add a new dependency, you'll have to restart GHCi for 70 | it to be picked up. 71 | 72 | In general, if something goes wrong you should run the "Purple Yolk: Start 73 | Interpreter" command to (re)start GHCi. If things go _really_ wrong you can try 74 | "Developer: Reload Window" to reload everything. 75 | 76 | Purple Yolk produces some chatty output that can help debugging. Use the 77 | "Purple Yolk: Show Output" command to see it. 78 | 79 | ## Development 80 | 81 | To build Purple Yolk locally and install it, run the following commands: 82 | 83 | ``` sh 84 | $ npm install 85 | $ rm -f purple-yolk-*.vsix 86 | $ npx vsce package --pre-release 87 | $ code --install-extension purple-yolk-*.vsix 88 | ``` 89 | -------------------------------------------------------------------------------- /data/grammar/cabal.json: -------------------------------------------------------------------------------- 1 | { 2 | "//": "https://code.visualstudio.com/api/language-extensions/syntax-highlight-guide", 3 | "patterns": [ 4 | { 5 | "include": "#comment" 6 | }, 7 | { 8 | "include": "#keyword" 9 | }, 10 | { 11 | "include": "#number" 12 | }, 13 | { 14 | "include": "#operator" 15 | } 16 | ], 17 | "repository": { 18 | "comment": { 19 | "match": "^\\s*--.*$", 20 | "name": "comment.line.double-dash.cabal" 21 | }, 22 | "keyword": { 23 | "match": "^\\s*\\S+:", 24 | "name": "keyword.other.cabal" 25 | }, 26 | "number": { 27 | "match": "\\b\\d+([.]\\d+)*\\b", 28 | "name": "constant.numeric.cabal" 29 | }, 30 | "operator": { 31 | "match": "==|>=|<=|>|<|\\|\\||&&|\\^>=", 32 | "name": "keyword.operator.cabal" 33 | } 34 | }, 35 | "scopeName": "source.cabal" 36 | } 37 | -------------------------------------------------------------------------------- /data/grammar/haskell.json: -------------------------------------------------------------------------------- 1 | { 2 | "//": "https://code.visualstudio.com/api/language-extensions/syntax-highlight-guide", 3 | "patterns": [ 4 | { 5 | "include": "#character" 6 | }, 7 | { 8 | "include": "#comment" 9 | }, 10 | { 11 | "include": "#number" 12 | }, 13 | { 14 | "include": "#reserved" 15 | }, 16 | { 17 | "include": "#identifier" 18 | }, 19 | { 20 | "include": "#string" 21 | }, 22 | { 23 | "include": "#operator" 24 | } 25 | ], 26 | "repository": { 27 | "character": { 28 | "captures": { 29 | "1": { 30 | "name": "constant.character.escape.haskell" 31 | } 32 | }, 33 | "match": "'(?:[^'\\\\n\\r]|(\\\\(?:[abfnrtv\\\\\"']|\\^[A-Z@\\[\\\\\\]\\^_]|(?:NUL|SOH|STX|ETX|EOT|ENQ|ACK|BEL|BS|HT|LF|VT|FF|CR|SO|SI|DLE|DC1|DC2|DC3|DC4|NAK|SYN|ETB|CAN|EM|SUB|ESC|FS|GS|RS|US|SP|DEL)|[0-9]+|o[0-7]+|x[0-9A-Fa-f]+)))'", 34 | "name": "constant.character.haskell" 35 | }, 36 | "comment": { 37 | "patterns": [ 38 | { 39 | "include": "#commentPragma" 40 | }, 41 | { 42 | "include": "#commentBlock" 43 | }, 44 | { 45 | "include": "#commentLine" 46 | } 47 | ] 48 | }, 49 | "commentBlock": { 50 | "begin": "{-", 51 | "end": "-}", 52 | "name": "comment.block.haskell", 53 | "patterns": [ 54 | { 55 | "include": "#commentBlock" 56 | } 57 | ] 58 | }, 59 | "commentLine": { 60 | "match": "(??@\\\\\\^|~:])-{2,}(?![-!#$%&*+./<=>?@\\\\\\^|~:]).*", 61 | "name": "comment.line.double-dash.haskell" 62 | }, 63 | "commentPragma": { 64 | "begin": "{-#", 65 | "end": "#-}", 66 | "name": "keyword.other.haskell" 67 | }, 68 | "identifier": { 69 | "patterns": [ 70 | { 71 | "include": "#identifierConstructor" 72 | } 73 | ] 74 | }, 75 | "identifierConstructor": { 76 | "match": "\\b[A-Z][a-z_A-Z0-9']*", 77 | "name": "storage.type.haskell" 78 | }, 79 | "number": { 80 | "match": "\\b(?:[0-9]+[eE][+-]?[0-9]+|[0-9]+[.][0-9]+(?:[eE][+-]?[0-9]+)?|0[oO][0-7]+|0[xX][0-9A-Fa-f]+|[0-9]+)\\b", 81 | "name": "constant.numeric.haskell" 82 | }, 83 | "operator": { 84 | "match": "\\~|\\`|\\!|\\@|\\#|\\$|\\%|\\^|\\&|\\*|\\(|\\)|\\-|\\_|\\+|\\=|\\{|\\}|\\[|\\]|\\\\|\\:|\\;|\\'|\\<|\\>|\\,|\\.|\\/|\\?", 85 | "name": "keyword.operator.haskell" 86 | }, 87 | "reserved": { 88 | "patterns": [ 89 | { 90 | "include": "#reservedSoft" 91 | }, 92 | { 93 | "include": "#reservedHard" 94 | } 95 | ] 96 | }, 97 | "reservedHard": { 98 | "match": "\\b(?:case|class|data|default|deriving|do|else|foreign|if|import|in|infix[lr]?|instance|let|module|newtype|of|then|type|where|_)\\b", 99 | "name": "keyword.control.haskell" 100 | }, 101 | "reservedSoft": { 102 | "match": "\\b(as|hiding|qualified)\\b", 103 | "name": "keyword.control.haskell" 104 | }, 105 | "string": { 106 | "begin": "\"", 107 | "end": "\"", 108 | "name": "string.quoted.double.haskell", 109 | "patterns": [ 110 | { 111 | "include": "#stringCharacter" 112 | } 113 | ] 114 | }, 115 | "stringCharacter": { 116 | "captures": { 117 | "1": { 118 | "name": "comment.block.haskell" 119 | }, 120 | "2": { 121 | "name": "constant.character.escape.haskell" 122 | } 123 | }, 124 | "match": "(^[ \\t]*\\\\|\\\\[ \\t]+\\\\|\\\\[ \\t]*$)|[^\"\\\\\\n\\r]|(\\\\(?:[abfnrtv\\\\\"'&]|\\^[A-Z@\\[\\\\\\]\\^_]|(?:NUL|SOH|STX|ETX|EOT|ENQ|ACK|BEL|BS|HT|LF|VT|FF|CR|SO|SI|DLE|DC1|DC2|DC3|DC4|NAK|SYN|ETB|CAN|EM|SUB|ESC|FS|GS|RS|US|SP|DEL)|[0-9]+|o[0-7]+|x[0-9A-Fa-f]+))" 125 | } 126 | }, 127 | "scopeName": "source.haskell" 128 | } 129 | -------------------------------------------------------------------------------- /data/image/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tfausak/purple-yolk/286e8325c438735ba449c8b8665f22ea8e998f33/data/image/icon.png -------------------------------------------------------------------------------- /data/language/cabal.json: -------------------------------------------------------------------------------- 1 | { 2 | "//": "https://code.visualstudio.com/api/language-extensions/language-configuration-guide", 3 | "autoClosingPairs": [ 4 | [ 5 | "'", 6 | "'" 7 | ], 8 | [ 9 | "\"", 10 | "\"" 11 | ], 12 | [ 13 | "(", 14 | ")" 15 | ], 16 | [ 17 | "[", 18 | "]" 19 | ], 20 | [ 21 | "{", 22 | "}" 23 | ] 24 | ], 25 | "brackets": [ 26 | [ 27 | "(", 28 | ")" 29 | ], 30 | [ 31 | "[", 32 | "]" 33 | ], 34 | [ 35 | "{", 36 | "}" 37 | ] 38 | ], 39 | "comments": { 40 | "blockComment": [], 41 | "lineComment": "--" 42 | }, 43 | "surroundingPairs": [ 44 | [ 45 | "'", 46 | "'" 47 | ], 48 | [ 49 | "\"", 50 | "\"" 51 | ], 52 | [ 53 | "(", 54 | ")" 55 | ], 56 | [ 57 | "[", 58 | "]" 59 | ], 60 | [ 61 | "{", 62 | "}" 63 | ] 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /data/language/haskell.json: -------------------------------------------------------------------------------- 1 | { 2 | "//": "https://code.visualstudio.com/api/language-extensions/language-configuration-guide", 3 | "autoClosingPairs": [ 4 | [ 5 | "'", 6 | "'" 7 | ], 8 | [ 9 | "`", 10 | "`" 11 | ], 12 | [ 13 | "\"", 14 | "\"" 15 | ], 16 | [ 17 | "(", 18 | ")" 19 | ], 20 | [ 21 | "[", 22 | "]" 23 | ], 24 | [ 25 | "{", 26 | "}" 27 | ] 28 | ], 29 | "brackets": [ 30 | [ 31 | "(", 32 | ")" 33 | ], 34 | [ 35 | "[", 36 | "]" 37 | ], 38 | [ 39 | "{", 40 | "}" 41 | ] 42 | ], 43 | "comments": { 44 | "blockComment": [ 45 | "{-", 46 | "-}" 47 | ], 48 | "lineComment": "--" 49 | }, 50 | "indentationRules": { 51 | "decreaseIndentPattern": "(?!)", 52 | "increaseIndentPattern": "\\b(?:else|do|in|let|of|then|where)$", 53 | "indentNextLinePattern": "(?!)", 54 | "unIndentedLinePattern": "(?!)" 55 | }, 56 | "surroundingPairs": [ 57 | [ 58 | "'", 59 | "'" 60 | ], 61 | [ 62 | "`", 63 | "`" 64 | ], 65 | [ 66 | "\"", 67 | "\"" 68 | ], 69 | [ 70 | "(", 71 | ")" 72 | ], 73 | [ 74 | "[", 75 | "]" 76 | ], 77 | [ 78 | "{", 79 | "}" 80 | ] 81 | ] 82 | } 83 | -------------------------------------------------------------------------------- /data/snippet/cabal.json: -------------------------------------------------------------------------------- 1 | { 2 | "//": "https://code.visualstudio.com/docs/editor/userdefinedsnippets", 3 | "conditional expression": { 4 | "body": [ 5 | "if ${1:predicate}", 6 | "\t${2:true}", 7 | "else", 8 | "\t${0:false}" 9 | ], 10 | "prefix": [ 11 | "if" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /data/snippet/haskell.json: -------------------------------------------------------------------------------- 1 | { 2 | "//": "https://code.visualstudio.com/docs/editor/userdefinedsnippets", 3 | "case expression": { 4 | "body": [ 5 | "case ${1:expression} of", 6 | "\t${2:pattern} -> ${0:expression}" 7 | ], 8 | "prefix": [ 9 | "case" 10 | ] 11 | }, 12 | "class declaration": { 13 | "body": [ 14 | "class ${1:Class} ${2:Type} where", 15 | "\t${3:method} :: ${0:Type}" 16 | ], 17 | "prefix": [ 18 | "class" 19 | ] 20 | }, 21 | "conditional expression": { 22 | "body": [ 23 | "if ${1:predicate} then ${2:true} else ${0:false}" 24 | ], 25 | "prefix": [ 26 | "if" 27 | ] 28 | }, 29 | "data declaration": { 30 | "body": [ 31 | "data ${1:Type} = ${0:Constructor}" 32 | ], 33 | "prefix": [ 34 | "data" 35 | ] 36 | }, 37 | "default declaration": { 38 | "body": [ 39 | "default ($0)" 40 | ], 41 | "prefix": [ 42 | "default" 43 | ] 44 | }, 45 | "do expression": { 46 | "body": [ 47 | "do", 48 | "\t${0:statement}" 49 | ], 50 | "prefix": [ 51 | "do" 52 | ] 53 | }, 54 | "fixity declaration": { 55 | "body": [ 56 | "${1|infix,infixl,infixr|} ${2|0,1,2,3,4,5,6,7,8,9|} ${0:operator}" 57 | ], 58 | "prefix": [ 59 | "infix" 60 | ] 61 | }, 62 | "foreign declaration": { 63 | "body": [ 64 | "foreign ${1|import,export|} ${2:ccall} \"${3:header}\" ${4:identifier} :: ${0:Type}" 65 | ], 66 | "prefix": [ 67 | "foreign" 68 | ] 69 | }, 70 | "import declaration": { 71 | "body": [ 72 | "import ${1:Module} (${0:identifier})" 73 | ], 74 | "prefix": [ 75 | "import" 76 | ] 77 | }, 78 | "instance declaration": { 79 | "body": [ 80 | "instance ${1:Class} ${2:Type} where", 81 | "\t${3:method} = ${0:expression}" 82 | ], 83 | "prefix": [ 84 | "instance" 85 | ] 86 | }, 87 | "let expression": { 88 | "body": [ 89 | "let ${1:pattern} = ${2:expression} in ${0:expression}" 90 | ], 91 | "prefix": [ 92 | "let" 93 | ] 94 | }, 95 | "module declaration": { 96 | "body": [ 97 | "module ${1:Module} (${2:export}) where", 98 | "", 99 | "$0" 100 | ], 101 | "prefix": [ 102 | "module" 103 | ] 104 | }, 105 | "newtype declaration": { 106 | "body": [ 107 | "newtype ${1:Type} = ${2:Constructor} ${0:Type}" 108 | ], 109 | "prefix": [ 110 | "newtype" 111 | ] 112 | }, 113 | "qualified import declaration": { 114 | "body": [ 115 | "import qualified ${1:Module} as ${0:Alias}" 116 | ], 117 | "prefix": [ 118 | "import" 119 | ] 120 | }, 121 | "type declaration": { 122 | "body": [ 123 | "type ${1:Alias} = ${0:Definition}" 124 | ], 125 | "prefix": [ 126 | "type" 127 | ] 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "//": "https://code.visualstudio.com/api/references/extension-manifest", 3 | "badges": [ 4 | { 5 | "description": "Build status", 6 | "href": "https://github.com/tfausak/purple-yolk/actions", 7 | "url": "https://github.com/tfausak/purple-yolk/workflows/Workflow/badge.svg" 8 | } 9 | ], 10 | "categories": [ 11 | "Formatters", 12 | "Linters", 13 | "Programming Languages", 14 | "Snippets" 15 | ], 16 | "contributes": { 17 | "commands": [ 18 | { 19 | "category": "Purple Yolk", 20 | "command": "purple-yolk.haskell.interpret", 21 | "title": "Start Interpreter", 22 | "when": "editorLangId == haskell" 23 | }, 24 | { 25 | "category": "Purple Yolk", 26 | "command": "purple-yolk.haskell.lint", 27 | "title": "Lint Document", 28 | "when": "editorLangId == haskell" 29 | }, 30 | { 31 | "category": "Purple Yolk", 32 | "command": "purple-yolk.output.show", 33 | "title": "Show Output" 34 | } 35 | ], 36 | "configuration": { 37 | "properties": { 38 | "purple-yolk.cabal.formatter.command": { 39 | "default": "", 40 | "description": "The command to run when formatting Cabal files. This should accept an unformatted Cabal file as input on STDIN. It should emit a formatted Cabal file to STDOUT. STDERR will be ignored.", 41 | "type": "string" 42 | }, 43 | "purple-yolk.cabal.formatter.mode": { 44 | "default": "discover", 45 | "description": "The command to format a Cabal file.", 46 | "enum": [ 47 | "discover", 48 | "cabal-fmt", 49 | "gild", 50 | "custom" 51 | ], 52 | "enumItemLabels": [ 53 | "Discover", 54 | "cabal-fmt", 55 | "Gild", 56 | "Custom" 57 | ], 58 | "markdownEnumDescriptions": [ 59 | "Automatically determines the appropriate command.", 60 | "Uses `cabal-fmt`.", 61 | "Uses `cabal-gild`.", 62 | "Uses the command in `#purple-yolk.cabal.formatter.command#`." 63 | ], 64 | "type": "string" 65 | }, 66 | "purple-yolk.haskell.formatter.command": { 67 | "default": "", 68 | "description": "The command to run when formatting Haskell files. This should accept an unformatted Haskell file as input on STDIN. It should emit a formatted Haskell file to STDOUT. STDERR will be ignored.", 69 | "type": "string" 70 | }, 71 | "purple-yolk.haskell.formatter.mode": { 72 | "default": "discover", 73 | "description": "The command to format a Haskell file.", 74 | "enum": [ 75 | "discover", 76 | "fourmolu", 77 | "ormolu", 78 | "custom" 79 | ], 80 | "enumItemLabels": [ 81 | "Discover", 82 | "Fourmolu", 83 | "Ormolu", 84 | "Custom" 85 | ], 86 | "markdownEnumDescriptions": [ 87 | "Automatically determines the appropriate command.", 88 | "Uses `fourmolu`.", 89 | "Uses `ormolu`.", 90 | "Uses the command in `#purple-yolk.haskell.formatter.command#`." 91 | ], 92 | "type": "string" 93 | }, 94 | "purple-yolk.haskell.interpreter.command": { 95 | "default": "", 96 | "markdownDescription": "This should pass `-fdiagnostics-as-json` (or `-ddump-json`) to GHC.", 97 | "type": "string" 98 | }, 99 | "purple-yolk.haskell.interpreter.mode": { 100 | "default": "discover", 101 | "description": "The command to launch a Haskell interpreter.", 102 | "enum": [ 103 | "discover", 104 | "cabal", 105 | "stack", 106 | "ghci", 107 | "custom" 108 | ], 109 | "enumItemLabels": [ 110 | "Discover", 111 | "Cabal", 112 | "Stack", 113 | "GHCi", 114 | "Custom" 115 | ], 116 | "markdownEnumDescriptions": [ 117 | "Automatically determines the appropriate command.", 118 | "Uses `cabal repl`.", 119 | "Uses `stack ghci`.", 120 | "Uses `ghci`. Only works with a single file.", 121 | "Uses the command in `#purple-yolk.haskell.interpreter.custom#`." 122 | ], 123 | "type": "string" 124 | }, 125 | "purple-yolk.haskell.linter.command": { 126 | "default": "", 127 | "description": "The command to run when linting Haskell files. This should accept a Haskell file as input on STDIN. It should emit HLint ideas formatted as JSON to STDOUT. STDERR will be ignored.", 128 | "type": "string" 129 | }, 130 | "purple-yolk.haskell.linter.mode": { 131 | "default": "discover", 132 | "description": "The command to lint a Haskell file.", 133 | "enum": [ 134 | "discover", 135 | "hlint", 136 | "custom" 137 | ], 138 | "enumItemLabels": [ 139 | "Discover", 140 | "HLint", 141 | "Custom" 142 | ], 143 | "markdownEnumDescriptions": [ 144 | "Automatically determines the appropriate command.", 145 | "Uses `hlint`.", 146 | "Uses the command in `#purple-yolk.haskell.linter.command#`." 147 | ], 148 | "type": "string" 149 | }, 150 | "purple-yolk.haskell.linter.onSave": { 151 | "default": true, 152 | "description": "Should the linter be run automatically on save?", 153 | "type": "boolean" 154 | } 155 | }, 156 | "title": "Purple Yolk" 157 | }, 158 | "grammars": [ 159 | { 160 | "language": "cabal", 161 | "path": "data/grammar/cabal.json", 162 | "scopeName": "source.cabal" 163 | }, 164 | { 165 | "language": "haskell", 166 | "path": "data/grammar/haskell.json", 167 | "scopeName": "source.haskell" 168 | } 169 | ], 170 | "languages": [ 171 | { 172 | "aliases": [ 173 | "Cabal" 174 | ], 175 | "configuration": "data/language/cabal.json", 176 | "extensions": [ 177 | ".cabal" 178 | ], 179 | "filenames": [ 180 | "cabal.project.freeze", 181 | "cabal.project.local", 182 | "cabal.project" 183 | ], 184 | "id": "cabal" 185 | }, 186 | { 187 | "aliases": [ 188 | "Haskell" 189 | ], 190 | "configuration": "data/language/haskell.json", 191 | "extensions": [ 192 | ".hs" 193 | ], 194 | "id": "haskell" 195 | } 196 | ], 197 | "snippets": [ 198 | { 199 | "language": "cabal", 200 | "path": "data/snippet/cabal.json" 201 | }, 202 | { 203 | "language": "haskell", 204 | "path": "data/snippet/haskell.json" 205 | } 206 | ] 207 | }, 208 | "description": "A simple IDE for Haskell projects.", 209 | "devDependencies": { 210 | "@tsconfig/node20": "^20.1.4", 211 | "@tsconfig/strictest": "^2.0.5", 212 | "@types/node": "^22.13.10", 213 | "@types/vscode": "^1.98.0", 214 | "@types/which": "^3.0.4", 215 | "@vscode/vsce": "^3.3.0", 216 | "esbuild": "^0.25.1", 217 | "typescript": "^5.8.2", 218 | "vscode-uri": "^3.1.0", 219 | "which": "^5.0.0" 220 | }, 221 | "displayName": "Purple Yolk", 222 | "engines": { 223 | "node": "^20.18.2", 224 | "vscode": "^1.98.2" 225 | }, 226 | "homepage": "https://github.com/tfausak/purple-yolk", 227 | "icon": "data/image/icon.png", 228 | "keywords": [ 229 | "haskell" 230 | ], 231 | "license": "MIT", 232 | "main": "dist/client.js", 233 | "name": "purple-yolk", 234 | "publisher": "taylorfausak", 235 | "repository": { 236 | "type": "git", 237 | "url": "https://github.com/tfausak/purple-yolk.git" 238 | }, 239 | "scripts": { 240 | "vscode:prepublish": "esbuild --bundle --external:vscode --outdir=dist --platform=node --target=node8 source/client.ts" 241 | }, 242 | "version": "1.1.0" 243 | } 244 | -------------------------------------------------------------------------------- /source/client.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert"; 2 | import childProcess from "child_process"; 3 | import path from "path"; 4 | import perfHooks from "perf_hooks"; 5 | import readline from "readline"; 6 | import vscode from "vscode"; 7 | import which from "which"; 8 | import { Utils } from "vscode-uri"; 9 | 10 | import CabalFormatterMode from "./type/CabalFormatterMode"; 11 | import HaskellFormatterMode from "./type/HaskellFormatterMode"; 12 | import HaskellLinterMode from "./type/HaskellLinterMode"; 13 | import Idea from "./type/Idea"; 14 | import IdeaSeverity from "./type/IdeaSeverity"; 15 | import Interpreter from "./type/Interpreter"; 16 | import InterpreterMode from "./type/InterpreterMode"; 17 | import Key from "./type/Key"; 18 | import LanguageId from "./type/LanguageId"; 19 | import NewMessage from "./type/NewMessage"; 20 | import NewMessageSeverity from "./type/NewMessageSeverity"; 21 | import NewMessageSpan from "./type/NewMessageSpan"; 22 | import OldMessage from "./type/OldMessage"; 23 | import OldMessageSeverity from "./type/OldMessageSeverity"; 24 | import OldMessageSpan from "./type/OldMessageSpan"; 25 | import Template from "./type/Template"; 26 | 27 | import my from "../package.json"; 28 | 29 | const DEFAULT_OLD_MESSAGE_SPAN: OldMessageSpan = { 30 | endCol: 1, 31 | endLine: 1, 32 | file: "", 33 | startCol: 1, 34 | startLine: 1, 35 | }; 36 | 37 | const DEFAULT_NEW_MESSAGE_SPAN: NewMessageSpan = { 38 | end: { 39 | column: 1, 40 | line: 1 41 | }, 42 | file: "", 43 | start: { 44 | column: 1, 45 | line: 1 46 | }, 47 | }; 48 | 49 | let INTERPRETER: Interpreter | null = null; 50 | 51 | let INTERPRETER_TEMPLATE: Template | undefined = undefined; 52 | 53 | let HASKELL_FORMATTER_TEMPLATE: Template | undefined = undefined; 54 | 55 | let HASKELL_LINTER_TEMPLATE: Template | undefined = undefined; 56 | 57 | let CABAL_FORMATTER_TEMPLATE: Template | undefined = undefined; 58 | 59 | // https://hackage.haskell.org/package/ghc-9.8.2/docs/GHC-Driver-Flags.html#v:warnFlagNames 60 | const GHC_WARNING_FLAGS: { [k: string]: string } = { 61 | Opt_WarnAllMissedSpecs: "all-missed-specialisations", 62 | Opt_WarnAlternativeLayoutRuleTransitional: "alternative-layout-rule-transitional", 63 | Opt_WarnAmbiguousFields: "ambiguous-fields", 64 | Opt_WarnAutoOrphans: "auto-orphans", 65 | Opt_WarnCompatUnqualifiedImports: "compat-unqualified-imports", 66 | Opt_WarnCPPUndef: "cpp-undef", 67 | Opt_WarnDeferredOutOfScopeVariables: "deferred-out-of-scope-variables", 68 | Opt_WarnDeferredTypeErrors: "deferred-type-errors", 69 | Opt_WarnDeprecatedFlags: "deprecated-flags", 70 | Opt_WarnDerivingDefaults: "deriving-defaults", 71 | Opt_WarnDerivingTypeable: "deriving-typeable", 72 | Opt_WarnDodgyExports: "dodgy-exports", 73 | Opt_WarnDodgyForeignImports: "dodgy-foreign-imports", 74 | Opt_WarnDodgyImports: "dodgy-imports", 75 | Opt_WarnDuplicateConstraints: "duplicate-constraints", 76 | Opt_WarnDuplicateExports: "duplicate-exports", 77 | Opt_WarnEmptyEnumerations: "empty-enumerations", 78 | Opt_WarnForallIdentifier: "forall-identifier", 79 | Opt_WarnGADTMonoLocalBinds: "gadt-mono-local-binds", 80 | Opt_WarnHiShadows: "hi-shadowing", 81 | Opt_WarnIdentities: "identities", 82 | Opt_WarnImplicitKindVars: "implicit-kind-vars", 83 | Opt_WarnImplicitLift: "implicit-lift", 84 | Opt_WarnImplicitPrelude: "implicit-prelude", 85 | Opt_WarnImplicitRhsQuantification: "implicit-rhs-quantification", 86 | Opt_WarnInaccessibleCode: "inaccessible-code", 87 | Opt_WarnIncompleteExportWarnings: "incomplete-export-warnings", 88 | Opt_WarnIncompletePatterns: "incomplete-patterns", 89 | Opt_WarnIncompletePatternsRecUpd: "incomplete-record-updates", 90 | Opt_WarnIncompleteUniPatterns: "incomplete-uni-patterns", 91 | Opt_WarnInconsistentFlags: "inconsistent-flags", 92 | Opt_WarnInferredSafeImports: "inferred-safe-imports", 93 | Opt_WarnInlineRuleShadowing: "inline-rule-shadowing", 94 | Opt_WarnInvalidHaddock: "invalid-haddock", 95 | Opt_WarnLoopySuperclassSolve: "loopy-superclass-solve", 96 | Opt_WarnMisplacedPragmas: "misplaced-pragmas", 97 | Opt_WarnMissedExtraSharedLib: "missed-extra-shared-lib", 98 | Opt_WarnMissedSpecs: "missed-specialisations", 99 | Opt_WarnMissingDerivingStrategies: "missing-deriving-strategies", 100 | Opt_WarnMissingExportedPatternSynonymSignatures: "missing-exported-pattern-synonym-signatures", 101 | Opt_WarnMissingExportedSignatures: "missing-exported-signatures", 102 | Opt_WarnMissingExportList: "missing-export-lists", 103 | Opt_WarnMissingFields: "missing-fields", 104 | Opt_WarnMissingHomeModules: "missing-home-modules", 105 | Opt_WarnMissingImportList: "missing-import-lists", 106 | Opt_WarnMissingKindSignatures: "missing-kind-signatures", 107 | Opt_WarnMissingLocalSignatures: "missing-local-signatures", 108 | Opt_WarnMissingMethods: "missing-methods", 109 | Opt_WarnMissingMonadFailInstances: "missing-monadfail-instances", 110 | Opt_WarnMissingPatternSynonymSignatures: "missing-pattern-synonym-signatures", 111 | Opt_WarnMissingPolyKindSignatures: "missing-poly-kind-signatures", 112 | Opt_WarnMissingRoleAnnotations: "missing-role-annotations", 113 | Opt_WarnMissingSafeHaskellMode: "missing-safe-haskell-mode", 114 | Opt_WarnMissingSignatures: "missing-signatures", 115 | Opt_WarnMonomorphism: "monomorphism-restriction", 116 | Opt_WarnNameShadowing: "name-shadowing", 117 | Opt_WarnNonCanonicalMonadFailInstances: "noncanonical-monadfail-instances", 118 | Opt_WarnNonCanonicalMonadInstances: "noncanonical-monad-instances", 119 | Opt_WarnNonCanonicalMonoidInstances: "noncanonical-monoid-instances", 120 | Opt_WarnOperatorWhitespace: "operator-whitespace", 121 | Opt_WarnOperatorWhitespaceExtConflict: "operator-whitespace-ext-conflict", 122 | Opt_WarnOrphans: "orphans", 123 | Opt_WarnOverflowedLiterals: "overflowed-literals", 124 | Opt_WarnOverlappingPatterns: "overlapping-patterns", 125 | Opt_WarnPartialFields: "partial-fields", 126 | Opt_WarnPartialTypeSignatures: "partial-type-signatures", 127 | Opt_WarnPrepositiveQualifiedModule: "prepositive-qualified-module", 128 | Opt_WarnRedundantBangPatterns: "redundant-bang-patterns", 129 | Opt_WarnRedundantConstraints: "redundant-constraints", 130 | Opt_WarnRedundantRecordWildcards: "redundant-record-wildcards", 131 | Opt_WarnRedundantStrictnessFlags: "redundant-strictness-flags", 132 | Opt_WarnSafe: "safe", 133 | Opt_WarnSemigroup: "semigroup", 134 | Opt_WarnSimplifiableClassConstraints: "simplifiable-class-constraints", 135 | Opt_WarnSpaceAfterBang: "missing-space-after-bang", 136 | Opt_WarnStarBinder: "star-binder", 137 | Opt_WarnStarIsType: "star-is-type", 138 | Opt_WarnTabs: "tabs", 139 | Opt_WarnTermVariableCapture: "term-variable-capture", 140 | Opt_WarnTrustworthySafe: "trustworthy-safe", 141 | Opt_WarnTypeDefaults: "type-defaults", 142 | Opt_WarnTypedHoles: "typed-holes", 143 | Opt_WarnTypeEqualityOutOfScope: "type-equality-out-of-scope", 144 | Opt_WarnTypeEqualityRequiresOperators: "type-equality-requires-operators", 145 | Opt_WarnUnbangedStrictPatterns: "unbanged-strict-patterns", 146 | Opt_WarnUnicodeBidirectionalFormatCharacters: "unicode-bidirectional-format-characters", 147 | Opt_WarnUnrecognisedPragmas: "unrecognised-pragmas", 148 | Opt_WarnUnrecognisedWarningFlags: "unrecognised-warning-flags", 149 | Opt_WarnUnsafe: "unsafe", 150 | Opt_WarnUnsupportedCallingConventions: "unsupported-calling-conventions", 151 | Opt_WarnUnsupportedLlvmVersion: "unsupported-llvm-version", 152 | Opt_WarnUntickedPromotedConstructors: "unticked-promoted-constructors", 153 | Opt_WarnUnusedDoBind: "unused-do-bind", 154 | Opt_WarnUnusedForalls: "unused-foralls", 155 | Opt_WarnUnusedImports: "unused-imports", 156 | Opt_WarnUnusedLocalBinds: "unused-local-binds", 157 | Opt_WarnUnusedMatches: "unused-matches", 158 | Opt_WarnUnusedPackages: "unused-packages", 159 | Opt_WarnUnusedPatternBinds: "unused-pattern-binds", 160 | Opt_WarnUnusedRecordWildcards: "unused-record-wildcards", 161 | Opt_WarnUnusedTopBinds: "unused-top-binds", 162 | Opt_WarnUnusedTypePatterns: "unused-type-patterns", 163 | Opt_WarnWrongDoBind: "wrong-do-bind", 164 | }; 165 | 166 | // GHC warnings that should get an "unnecessary" tag. 167 | const UNNECESSARY_WARNINGS = new Set([ 168 | "Opt_WarnDuplicateConstraints", GHC_WARNING_FLAGS['Opt_WarnDuplicateConstraints'], 169 | "Opt_WarnDuplicateExports", GHC_WARNING_FLAGS['Opt_WarnDuplicateExports'], 170 | "Opt_WarnRedundantBangPatterns", GHC_WARNING_FLAGS['Opt_WarnRedundantBangPatterns'], 171 | "Opt_WarnRedundantConstraints", GHC_WARNING_FLAGS['Opt_WarnRedundantConstraints'], 172 | "Opt_WarnRedundantRecordWildcards", GHC_WARNING_FLAGS['Opt_WarnRedundantRecordWildcards'], 173 | "Opt_WarnRedundantStrictnessFlags", GHC_WARNING_FLAGS['Opt_WarnRedundantStrictnessFlags'], 174 | "Opt_WarnUnusedDoBind", GHC_WARNING_FLAGS['Opt_WarnUnusedDoBind'], 175 | "Opt_WarnUnusedForalls", GHC_WARNING_FLAGS['Opt_WarnUnusedForalls'], 176 | "Opt_WarnUnusedImports", GHC_WARNING_FLAGS['Opt_WarnUnusedImports'], 177 | "Opt_WarnUnusedLocalBinds", GHC_WARNING_FLAGS['Opt_WarnUnusedLocalBinds'], 178 | "Opt_WarnUnusedMatches", GHC_WARNING_FLAGS['Opt_WarnUnusedMatches'], 179 | "Opt_WarnUnusedPackages", GHC_WARNING_FLAGS['Opt_WarnUnusedPackages'], 180 | "Opt_WarnUnusedPatternBinds", GHC_WARNING_FLAGS['Opt_WarnUnusedPatternBinds'], 181 | "Opt_WarnUnusedRecordWildcards", GHC_WARNING_FLAGS['Opt_WarnUnusedRecordWildcards'], 182 | "Opt_WarnUnusedTopBinds", GHC_WARNING_FLAGS['Opt_WarnUnusedTopBinds'], 183 | "Opt_WarnUnusedTypePatterns", GHC_WARNING_FLAGS['Opt_WarnUnusedTypePatterns'], 184 | ]); 185 | 186 | // GHC warnings that should get a "deprecated" tag. 187 | const DEPRECATED_WARNINGS = new Set([ 188 | "GHC-15328", "deprecations", 189 | "GHC-63394", "x-partial", 190 | "Opt_WarnDeprecatedFlags", GHC_WARNING_FLAGS['Opt_WarnDeprecatedFlags'], 191 | ]); 192 | 193 | const COMPILING_PATTERN = /^\[ *(\d+) of (\d+)\] Compiling ([^ ]+) +\( ([^,]+)/; 194 | 195 | function discoverInterpreterMode( 196 | cabal: string | null, 197 | cabalProject: vscode.Uri | undefined, // cabal.project 198 | cabalPackage: vscode.Uri | undefined, // *.cabal 199 | ghci: string | null, 200 | stack: string | null, 201 | stackProject: vscode.Uri | undefined, // stack.yaml 202 | stackPackage: vscode.Uri | undefined // package.yaml 203 | ): InterpreterMode { 204 | // If the user has GHCi installed and there are no Cabal or Stack files, then 205 | // use GHCi. 206 | if (ghci && !cabalProject && !cabalPackage && !stackProject && !stackPackage) { 207 | return InterpreterMode.Ghci; 208 | } 209 | 210 | if (cabal && !stack) { 211 | // If the user only has Cabal available, then use Cabal. 212 | return InterpreterMode.Cabal; 213 | } 214 | 215 | if (!cabal && stack) { 216 | // If the user only has Stack available, then use Stack. 217 | return InterpreterMode.Stack; 218 | } 219 | 220 | if (cabal && stack) { 221 | if (!cabalProject && stackProject) { 222 | // If the user has both Cabal and Stack installed, but they only have a 223 | // Stack project file, then use Stack. 224 | return InterpreterMode.Stack; 225 | } 226 | 227 | // Otherwise use Cabal. 228 | return InterpreterMode.Cabal; 229 | } 230 | 231 | if (ghci) { 232 | // If the user has neither Cabal nor Stack installed, then attempt to use 233 | // GHCi. 234 | return InterpreterMode.Ghci; 235 | } 236 | 237 | return InterpreterMode.Discover; 238 | } 239 | 240 | async function setInterpreterTemplate( 241 | channel: vscode.OutputChannel 242 | ): Promise { 243 | const key = newKey(); 244 | log(channel, key, "Getting Haskell interpreter ..."); 245 | 246 | let mode: InterpreterMode | undefined = vscode.workspace 247 | .getConfiguration(my.name) 248 | .get(`${LanguageId.Haskell}.interpreter.mode`); 249 | log(channel, key, `Requested Haskell interpreter mode is ${mode}`); 250 | 251 | const custom: Template | undefined = vscode.workspace 252 | .getConfiguration(my.name) 253 | .get(`${LanguageId.Haskell}.interpreter.command`); 254 | 255 | if (mode === InterpreterMode.Discover) { 256 | if (custom) { 257 | mode = InterpreterMode.Custom; 258 | } else { 259 | const [cabal, [cabalProject], [cabalPackage], stack, [stackProject], [stackPackage], ghci] = 260 | await Promise.all([ 261 | which("cabal", { nothrow: true }), 262 | vscode.workspace.findFiles("cabal.project", undefined, 1), 263 | vscode.workspace.findFiles("*.cabal", undefined, 1), 264 | which("stack", { nothrow: true }), 265 | vscode.workspace.findFiles("stack.yaml", undefined, 1), 266 | vscode.workspace.findFiles("package.yaml", undefined, 1), 267 | which("ghci", { nothrow: true }), 268 | ]); 269 | 270 | mode = discoverInterpreterMode( 271 | cabal, 272 | cabalProject, 273 | cabalPackage, 274 | ghci, 275 | stack, 276 | stackProject, 277 | stackPackage 278 | ); 279 | } 280 | } 281 | 282 | if (mode === InterpreterMode.Stack) { 283 | const [stackProject] = await vscode.workspace.findFiles("stack.yaml", undefined, 1); 284 | if (!stackProject) { 285 | mode = InterpreterMode.StackNonProject; 286 | } 287 | } 288 | 289 | log(channel, key, `Actual Haskell interpreter mode is ${mode}`); 290 | 291 | switch (mode) { 292 | case InterpreterMode.Cabal: 293 | INTERPRETER_TEMPLATE = "cabal repl --repl-options -ddump-json"; 294 | break; 295 | case InterpreterMode.Stack: 296 | INTERPRETER_TEMPLATE = "stack ghci --ghci-options -ddump-json"; 297 | break; 298 | case InterpreterMode.StackNonProject: 299 | INTERPRETER_TEMPLATE = "stack ghci --ghci-options -ddump-json ${file}"; 300 | break; 301 | case InterpreterMode.Ghci: 302 | INTERPRETER_TEMPLATE = "ghci -ddump-json ${file}"; 303 | break; 304 | case InterpreterMode.Custom: 305 | INTERPRETER_TEMPLATE = custom; 306 | break; 307 | default: 308 | INTERPRETER_TEMPLATE = undefined; 309 | break; 310 | } 311 | } 312 | 313 | function discoverHaskellFormatterMode( 314 | fourmolu: string | null, 315 | fourmoluConfig: vscode.Uri | undefined, 316 | ormolu: string | null, 317 | ormoluConfig: vscode.Uri | undefined 318 | ): HaskellFormatterMode { 319 | if (fourmolu && !ormolu) { 320 | // If the user only has Fourmolu available, then use Fourmolu. 321 | return HaskellFormatterMode.Fourmolu; 322 | } 323 | 324 | if (!fourmolu && ormolu) { 325 | // If the user only has Ormolu available, then use Ormolu. 326 | return HaskellFormatterMode.Ormolu; 327 | } 328 | 329 | if (fourmolu && ormolu) { 330 | if (fourmoluConfig && !ormoluConfig) { 331 | // If the user has both Fourmolu and Ormolu installed, but they only 332 | // have a Fourmolu config file, then use Fourmolu. 333 | return HaskellFormatterMode.Fourmolu; 334 | } 335 | 336 | // Otherwise use Ormolu. 337 | return HaskellFormatterMode.Ormolu; 338 | } 339 | 340 | return HaskellFormatterMode.Discover; 341 | } 342 | 343 | async function setHaskellFormatterTemplate( 344 | channel: vscode.OutputChannel 345 | ): Promise { 346 | const key = newKey(); 347 | log(channel, key, "Getting Haskell formatter ..."); 348 | 349 | let mode: HaskellFormatterMode | undefined = vscode.workspace 350 | .getConfiguration(my.name) 351 | .get(`${LanguageId.Haskell}.formatter.mode`); 352 | log(channel, key, `Requested Haskell formatter mode is ${mode}`); 353 | 354 | const custom: Template | undefined = vscode.workspace 355 | .getConfiguration(my.name) 356 | .get(`${LanguageId.Haskell}.formatter.command`); 357 | 358 | if (mode === HaskellFormatterMode.Discover) { 359 | if (custom) { 360 | mode = HaskellFormatterMode.Custom; 361 | } else { 362 | const [fourmolu, [fourmoluConfig], ormolu, [ormoluConfig]] = 363 | await Promise.all([ 364 | which("fourmolu", { nothrow: true }), 365 | vscode.workspace.findFiles("fourmolu.yaml", undefined, 1), 366 | which("ormolu", { nothrow: true }), 367 | vscode.workspace.findFiles(".ormolu", undefined, 1), 368 | ]); 369 | 370 | mode = discoverHaskellFormatterMode( 371 | fourmolu, 372 | fourmoluConfig, 373 | ormolu, 374 | ormoluConfig 375 | ); 376 | } 377 | } 378 | log(channel, key, `Actual Haskell formatter mode is ${mode}`); 379 | 380 | switch (mode) { 381 | case HaskellFormatterMode.Fourmolu: 382 | HASKELL_FORMATTER_TEMPLATE = "fourmolu --stdin-input-file ${file}"; 383 | break; 384 | case HaskellFormatterMode.Ormolu: 385 | HASKELL_FORMATTER_TEMPLATE = "ormolu --stdin-input-file ${file}"; 386 | break; 387 | case HaskellFormatterMode.Custom: 388 | HASKELL_FORMATTER_TEMPLATE = custom; 389 | break; 390 | default: 391 | HASKELL_FORMATTER_TEMPLATE = undefined; 392 | break; 393 | } 394 | } 395 | 396 | function discoverHaskellLinterMode( 397 | hlint: string | null 398 | ): HaskellLinterMode { 399 | if (hlint) { 400 | return HaskellLinterMode.Hlint; 401 | } 402 | 403 | return HaskellLinterMode.Discover; 404 | } 405 | 406 | async function setHaskellLinterTemplate( 407 | channel: vscode.OutputChannel 408 | ): Promise { 409 | const key = newKey(); 410 | log(channel, key, "Getting Haskell linter ..."); 411 | 412 | let mode: HaskellLinterMode | undefined = vscode.workspace 413 | .getConfiguration(my.name) 414 | .get(`${LanguageId.Haskell}.linter.mode`); 415 | log(channel, key, `Requested Haskell linter mode is ${mode}`); 416 | 417 | const custom: Template | undefined = vscode.workspace 418 | .getConfiguration(my.name) 419 | .get(`${LanguageId.Haskell}.linter.command`); 420 | 421 | if (mode === HaskellLinterMode.Discover) { 422 | if (custom) { 423 | mode = HaskellLinterMode.Custom; 424 | } else { 425 | const hlint = await which("hlint", { nothrow: true }); 426 | 427 | mode = discoverHaskellLinterMode(hlint); 428 | } 429 | } 430 | log(channel, key, `Actual Haskell linter mode is ${mode}`); 431 | 432 | switch (mode) { 433 | case HaskellLinterMode.Hlint: 434 | HASKELL_LINTER_TEMPLATE = "hlint --json --no-exit-code -"; 435 | break; 436 | case HaskellLinterMode.Custom: 437 | HASKELL_LINTER_TEMPLATE = custom; 438 | break; 439 | default: 440 | HASKELL_LINTER_TEMPLATE = undefined; 441 | break; 442 | } 443 | } 444 | 445 | function discoverCabalFormatterMode( 446 | cabalFmt: string | null, 447 | gild: string | null 448 | ): CabalFormatterMode { 449 | if (gild) { 450 | return CabalFormatterMode.Gild; 451 | } 452 | 453 | if (cabalFmt) { 454 | return CabalFormatterMode.CabalFmt; 455 | } 456 | 457 | return CabalFormatterMode.Discover; 458 | } 459 | 460 | async function setCabalFormatterTemplate( 461 | channel: vscode.OutputChannel 462 | ): Promise { 463 | const key = newKey(); 464 | log(channel, key, "Getting Cabal formatter ..."); 465 | 466 | let mode: CabalFormatterMode | undefined = vscode.workspace 467 | .getConfiguration(my.name) 468 | .get(`${LanguageId.Cabal}.formatter.mode`); 469 | log(channel, key, `Requested Cabal formatter mode is ${mode}`); 470 | 471 | const custom: Template | undefined = vscode.workspace 472 | .getConfiguration(my.name) 473 | .get(`${LanguageId.Cabal}.formatter.command`); 474 | 475 | if (mode === CabalFormatterMode.Discover) { 476 | if (custom) { 477 | mode = CabalFormatterMode.Custom; 478 | } else { 479 | const [cabalFmt, gild] = 480 | await Promise.all([ 481 | which("cabal-fmt", { nothrow: true }), 482 | which("cabal-gild", { nothrow: true }), 483 | ]); 484 | 485 | mode = discoverCabalFormatterMode(cabalFmt, gild); 486 | } 487 | } 488 | log(channel, key, `Actual Cabal formatter mode is ${mode}`); 489 | 490 | switch (mode) { 491 | case CabalFormatterMode.CabalFmt: 492 | CABAL_FORMATTER_TEMPLATE = "cabal-fmt --no-cabal-file --no-tabular"; 493 | break; 494 | case CabalFormatterMode.Gild: 495 | CABAL_FORMATTER_TEMPLATE = "cabal-gild --stdin ${file}"; 496 | break; 497 | case CabalFormatterMode.Custom: 498 | CABAL_FORMATTER_TEMPLATE = custom; 499 | break; 500 | default: 501 | CABAL_FORMATTER_TEMPLATE = undefined; 502 | break; 503 | } 504 | } 505 | 506 | function updateStatus( 507 | status: vscode.LanguageStatusItem, 508 | busy: boolean, 509 | severity: vscode.LanguageStatusSeverity, 510 | text: string, 511 | ) { 512 | status.busy = busy; 513 | status.severity = severity; 514 | status.text = text; 515 | } 516 | 517 | export async function activate( 518 | context: vscode.ExtensionContext 519 | ): Promise { 520 | const document = vscode.window.activeTextEditor?.document; 521 | 522 | const channel = vscode.window.createOutputChannel(my.displayName); 523 | const key = newKey(); 524 | const start = perfHooks.performance.now(); 525 | log(channel, key, `Activating ${my.name} version ${my.version} ...`); 526 | 527 | const collections = { 528 | ghc: vscode.languages.createDiagnosticCollection("ghc"), 529 | hlint: vscode.languages.createDiagnosticCollection("hlint"), 530 | }; 531 | 532 | const status = vscode.languages.createLanguageStatusItem( 533 | my.name, 534 | LanguageId.Haskell 535 | ); 536 | status.command = { command: `${my.name}.output.show`, title: "Show Output" }; 537 | status.name = my.displayName; 538 | updateStatus(status, false, vscode.LanguageStatusSeverity.Information, "Idle"); 539 | 540 | context.subscriptions.push( 541 | vscode.commands.registerCommand( 542 | `${my.name}.${LanguageId.Haskell}.interpret`, 543 | () => commandHaskellInterpret(channel, status, collections.ghc) 544 | ) 545 | ); 546 | 547 | context.subscriptions.push( 548 | vscode.commands.registerCommand( 549 | `${my.name}.${LanguageId.Haskell}.lint`, 550 | () => commandHaskellLint(channel, collections.hlint) 551 | ) 552 | ); 553 | 554 | context.subscriptions.push( 555 | vscode.commands.registerCommand(`${my.name}.output.show`, () => 556 | commandOutputShow(channel) 557 | ) 558 | ); 559 | 560 | vscode.workspace.onDidSaveTextDocument((document) => { 561 | switch (document.languageId) { 562 | case LanguageId.Haskell: 563 | reloadInterpreter(channel, status, collections.ghc); 564 | 565 | const shouldLint: boolean | undefined = vscode.workspace 566 | .getConfiguration(my.name) 567 | .get(`${document.languageId}.linter.onSave`); 568 | if (shouldLint) { 569 | commandHaskellLint(channel, collections.hlint); 570 | } 571 | 572 | break; 573 | } 574 | }); 575 | 576 | const languageIds = [LanguageId.Cabal, LanguageId.Haskell]; 577 | languageIds.forEach((languageId: LanguageId) => { 578 | vscode.languages.registerDocumentFormattingEditProvider(languageId, { 579 | provideDocumentFormattingEdits: (document, _, token) => 580 | formatDocument(languageId, channel, document, token), 581 | }); 582 | 583 | vscode.languages.registerDocumentRangeFormattingEditProvider(languageId, { 584 | provideDocumentRangeFormattingEdits: (document, range, _, token) => 585 | formatDocumentRange(languageId, channel, document, range, token), 586 | }); 587 | }); 588 | 589 | await Promise.all([ 590 | setInterpreterTemplate(channel), 591 | setHaskellFormatterTemplate(channel), 592 | setHaskellLinterTemplate(channel), 593 | setCabalFormatterTemplate(channel), 594 | ]); 595 | vscode.workspace.onDidChangeConfiguration(async (e) => { 596 | const promises = []; 597 | 598 | const affectsHaskellInterpreter = e.affectsConfiguration( 599 | `${my.name}.${LanguageId.Haskell}.interpreter` 600 | ); 601 | if (affectsHaskellInterpreter) { 602 | promises.push(setInterpreterTemplate(channel)); 603 | } 604 | 605 | const affectsHaskellFormatter = e.affectsConfiguration( 606 | `${my.name}.${LanguageId.Haskell}.formatter` 607 | ); 608 | if (affectsHaskellFormatter) { 609 | promises.push(setHaskellFormatterTemplate(channel)); 610 | } 611 | 612 | const affectsHaskellLinter = e.affectsConfiguration( 613 | `${my.name}.${LanguageId.Haskell}.linter` 614 | ); 615 | if (affectsHaskellLinter) { 616 | promises.push(setHaskellLinterTemplate(channel)); 617 | } 618 | 619 | const affectsCabalFormatter = e.affectsConfiguration( 620 | `${my.name}.${LanguageId.Cabal}.formatter` 621 | ); 622 | if (affectsCabalFormatter) { 623 | promises.push(setCabalFormatterTemplate(channel)); 624 | } 625 | 626 | await Promise.all(promises); 627 | }); 628 | 629 | if (document) { 630 | // If the user had a document open when the extension was activated, then 631 | // it can be used for starting the interpreter. 632 | startInterpreter(channel, status, collections.ghc, document); 633 | } else { 634 | // Otherwise the interpreter command can be run, which will determine the 635 | // current active document (if there is one). 636 | commandHaskellInterpret(channel, status, collections.ghc); 637 | } 638 | 639 | const end = perfHooks.performance.now(); 640 | const elapsed = ((end - start) / 1000).toFixed(3); 641 | log(channel, key, `Successfully activated in ${elapsed} seconds.`); 642 | } 643 | 644 | function commandHaskellInterpret( 645 | channel: vscode.OutputChannel, 646 | status: vscode.LanguageStatusItem, 647 | collection: vscode.DiagnosticCollection 648 | ): void { 649 | const document = vscode.window.activeTextEditor?.document; 650 | if (!document) { 651 | return; 652 | } 653 | 654 | startInterpreter(channel, status, collection, document); 655 | } 656 | 657 | function commandHaskellLint( 658 | channel: vscode.OutputChannel, 659 | collection: vscode.DiagnosticCollection 660 | ): void { 661 | const document = vscode.window.activeTextEditor?.document; 662 | if (!document || document.languageId !== LanguageId.Haskell) { 663 | return; 664 | } 665 | 666 | collection.delete(document.uri); 667 | 668 | vscode.window.withProgress( 669 | { 670 | cancellable: true, 671 | location: vscode.ProgressLocation.Window, 672 | title: `Linting`, 673 | }, 674 | async (progress, token) => { 675 | progress.report({ 676 | message: vscode.workspace.asRelativePath(document.uri), 677 | }); 678 | const diagnostics = await lintHaskell(channel, document, token); 679 | collection.set(document.uri, diagnostics); 680 | } 681 | ); 682 | } 683 | 684 | function commandOutputShow(channel: vscode.OutputChannel): void { 685 | channel.show(true); 686 | } 687 | 688 | function formatDocument( 689 | languageId: LanguageId, 690 | channel: vscode.OutputChannel, 691 | document: vscode.TextDocument, 692 | token: vscode.CancellationToken 693 | ): Promise { 694 | const range: vscode.Range = document.validateRange( 695 | new vscode.Range( 696 | new vscode.Position(0, 0), 697 | new vscode.Position(Infinity, Infinity) 698 | ) 699 | ); 700 | return formatDocumentRange(languageId, channel, document, range, token); 701 | } 702 | 703 | function expandTemplate( 704 | template: Template, 705 | replacements: { [key: string]: string } 706 | ): string { 707 | return template.replace(/\$\{(.*?)\}/, (_, key) => { 708 | const value = replacements[key]; 709 | if (typeof value === "undefined") { 710 | throw `unknown variable: ${key}`; 711 | } 712 | return value; 713 | }); 714 | } 715 | 716 | const getRootUri = (uri: vscode.Uri): vscode.Uri => 717 | vscode.workspace.getWorkspaceFolder(uri)?.uri ?? Utils.dirname(uri); 718 | 719 | async function formatDocumentRange( 720 | languageId: LanguageId, 721 | channel: vscode.OutputChannel, 722 | document: vscode.TextDocument, 723 | range: vscode.Range, 724 | token: vscode.CancellationToken 725 | ): Promise { 726 | const key = newKey(); 727 | const start = perfHooks.performance.now(); 728 | const file = vscode.workspace.asRelativePath(document.uri); 729 | log(channel, key, `Formatting ${file} using language ${languageId} ...`); 730 | 731 | const rootUri = getRootUri(document.uri); 732 | 733 | let template: Template | undefined = undefined; 734 | if (languageId === LanguageId.Haskell) { 735 | template = HASKELL_FORMATTER_TEMPLATE; 736 | } else if (languageId === LanguageId.Cabal) { 737 | template = CABAL_FORMATTER_TEMPLATE; 738 | } 739 | if (!template) { 740 | log(channel, key, "Error: Missing formatter command!"); 741 | return []; 742 | } 743 | 744 | const command = expandTemplate(template, { file }); 745 | const cwd = rootUri.fsPath; 746 | log( 747 | channel, 748 | key, 749 | `Running ${JSON.stringify(command)} in ${JSON.stringify(cwd)} ...` 750 | ); 751 | const task: childProcess.ChildProcess = childProcess.spawn(command, { 752 | cwd, 753 | shell: true, 754 | }); 755 | 756 | assert.ok(task.stderr); 757 | readline.createInterface(task.stderr).on("line", (line) => { 758 | log(channel, key, `[stderr] ${line}`); 759 | }); 760 | 761 | let output = ""; 762 | task.stdout?.on("data", (data) => (output += data)); 763 | 764 | token.onCancellationRequested(() => { 765 | log(channel, key, "Cancelling ..."); 766 | task.kill(); 767 | }); 768 | 769 | task.stdin?.end(document.getText(range)); 770 | 771 | const code: number = await new Promise((resolve) => 772 | task.on("close", resolve) 773 | ); 774 | if (code !== 0) { 775 | log(channel, key, `Error: Formatter exited with ${code}!`); 776 | if (!task.killed) { 777 | vscode.window.showErrorMessage(`Failed to format ${file}!`); 778 | } 779 | return []; 780 | } 781 | 782 | const end = perfHooks.performance.now(); 783 | const elapsed = ((end - start) / 1000).toFixed(3); 784 | log(channel, key, `Successfully formatted in ${elapsed} seconds.`); 785 | return [new vscode.TextEdit(range, output)]; 786 | } 787 | 788 | function ideaSeverityToDiagnostic( 789 | severity: IdeaSeverity 790 | ): vscode.DiagnosticSeverity { 791 | switch (severity) { 792 | case IdeaSeverity.Ignore: 793 | return vscode.DiagnosticSeverity.Hint; 794 | default: 795 | return vscode.DiagnosticSeverity.Information; 796 | } 797 | } 798 | 799 | function ideaToDiagnostic(idea: Idea): vscode.Diagnostic { 800 | const range = ideaToRange(idea); 801 | const message = ideaToMessage(idea); 802 | const diagnosticSeverity = ideaSeverityToDiagnostic(idea.severity); 803 | const diagnostic = new vscode.Diagnostic(range, message, diagnosticSeverity); 804 | diagnostic.source = "hlint"; 805 | return diagnostic; 806 | } 807 | 808 | function ideaToMessage(idea: Idea): string { 809 | const lines: string[] = [idea.hint]; 810 | if (idea.to) { 811 | lines.push(`Why not: ${idea.to}`); 812 | } 813 | for (const note of idea.note) { 814 | lines.push(`Note: ${note}`); 815 | } 816 | return lines.join("\n"); 817 | } 818 | 819 | function ideaToRange(idea: Idea): vscode.Range { 820 | return new vscode.Range( 821 | new vscode.Position(idea.startLine - 1, idea.startColumn - 1), 822 | new vscode.Position(idea.endLine - 1, idea.endColumn - 1) 823 | ); 824 | } 825 | 826 | async function lintHaskell( 827 | channel: vscode.OutputChannel, 828 | document: vscode.TextDocument, 829 | token: vscode.CancellationToken 830 | ): Promise { 831 | const key = newKey(); 832 | const start = perfHooks.performance.now(); 833 | const file = vscode.workspace.asRelativePath(document.uri); 834 | log(channel, key, `Linting ${file} ...`); 835 | 836 | const rootUri = getRootUri(document.uri); 837 | 838 | if (!HASKELL_LINTER_TEMPLATE) { 839 | log(channel, key, "Error: Missing linter command!"); 840 | return []; 841 | } 842 | 843 | const command = expandTemplate(HASKELL_LINTER_TEMPLATE, { file }); 844 | const cwd = rootUri.fsPath; 845 | log( 846 | channel, 847 | key, 848 | `Running ${JSON.stringify(command)} in ${JSON.stringify(cwd)} ...` 849 | ); 850 | const task: childProcess.ChildProcess = childProcess.spawn(command, { 851 | cwd, 852 | shell: true, 853 | }); 854 | 855 | assert.ok(task.stderr); 856 | readline.createInterface(task.stderr).on("line", (line) => { 857 | log(channel, key, `[stderr] ${line}`); 858 | }); 859 | 860 | let output = ""; 861 | task.stdout?.on("data", (data) => (output += data)); 862 | 863 | token.onCancellationRequested(() => { 864 | log(channel, key, "Cancelling ..."); 865 | task.kill(); 866 | }); 867 | 868 | task.stdin?.end(document.getText()); 869 | 870 | const code: number = await new Promise((resolve) => 871 | task.on("close", resolve) 872 | ); 873 | if (code !== 0) { 874 | log(channel, key, `Error: Linter exited with ${code}!`); 875 | if (!task.killed) { 876 | vscode.window.showErrorMessage(`Failed to lint ${file}!`); 877 | } 878 | return []; 879 | } 880 | 881 | let ideas: Idea[]; 882 | try { 883 | ideas = JSON.parse(output); 884 | } catch (error) { 885 | log(channel, key, `Error: ${error}`); 886 | vscode.window.showErrorMessage(`Failed to lint ${file}!`); 887 | return []; 888 | } 889 | 890 | const end = perfHooks.performance.now(); 891 | const elapsed = ((end - start) / 1000).toFixed(3); 892 | log(channel, key, `Successfully linted in ${elapsed} seconds.`); 893 | return ideas.map(ideaToDiagnostic); 894 | } 895 | 896 | function log(channel: vscode.OutputChannel, key: Key, message: string): void { 897 | channel.appendLine(`${new Date().toISOString()} [${key}] ${message}`); 898 | } 899 | 900 | function oldMessageSeverityToDiagnostic( 901 | severity: OldMessageSeverity 902 | ): vscode.DiagnosticSeverity { 903 | switch (severity) { 904 | case OldMessageSeverity.SevError: 905 | return vscode.DiagnosticSeverity.Error; 906 | case OldMessageSeverity.SevFatal: 907 | return vscode.DiagnosticSeverity.Error; 908 | case OldMessageSeverity.SevWarning: 909 | return vscode.DiagnosticSeverity.Warning; 910 | default: 911 | return vscode.DiagnosticSeverity.Information; 912 | } 913 | } 914 | 915 | function newMessageSeverityToDiagnostic( 916 | severity: NewMessageSeverity 917 | ): vscode.DiagnosticSeverity { 918 | switch (severity) { 919 | case NewMessageSeverity.Error: 920 | return vscode.DiagnosticSeverity.Error; 921 | case NewMessageSeverity.Warning: 922 | return vscode.DiagnosticSeverity.Warning; 923 | default: 924 | return vscode.DiagnosticSeverity.Information; 925 | } 926 | } 927 | 928 | function messageClassToDiagnosticSeverity( 929 | messageClass: string 930 | ): vscode.DiagnosticSeverity { 931 | const severities = new Set(Object.values(OldMessageSeverity)); 932 | for (const klass of messageClass.split(/ +/)) { 933 | const severity = klass as OldMessageSeverity; 934 | if (severities.has(severity)) { 935 | return oldMessageSeverityToDiagnostic(severity); 936 | } 937 | } 938 | return vscode.DiagnosticSeverity.Information; 939 | } 940 | 941 | function oldMessageSpanToRange(span: OldMessageSpan): vscode.Range { 942 | return new vscode.Range( 943 | new vscode.Position(span.startLine - 1, span.startCol - 1), 944 | new vscode.Position(span.endLine - 1, span.endCol - 1) 945 | ); 946 | } 947 | 948 | function newMessageSpanToRange(span: NewMessageSpan): vscode.Range { 949 | return new vscode.Range( 950 | new vscode.Position(span.start.line - 1, span.start.column - 1), 951 | new vscode.Position(span.end.line - 1, span.end.column - 1) 952 | ); 953 | } 954 | 955 | type DiagnosticCode 956 | = string 957 | | { value: string, target: vscode.Uri }; 958 | 959 | function makeDiagnosticCode(classes: string[]): DiagnosticCode { 960 | let reason: string | undefined; 961 | let code: string | undefined; 962 | for (const klass of classes) { 963 | reason ||= GHC_WARNING_FLAGS[klass]; 964 | code ||= (klass.match(/^GHC-\d+$/) || [])[0]; 965 | } 966 | 967 | reason ||= code || classes.join(" ") || "unknown"; 968 | if (!code) { 969 | return reason; 970 | } 971 | 972 | return { 973 | value: reason, 974 | target: vscode.Uri.parse(`https://errors.haskell.org/messages/${code}/`), 975 | }; 976 | } 977 | 978 | function makeDiagnosticTags(classes: string[]): vscode.DiagnosticTag[] { 979 | const tags: vscode.DiagnosticTag[] = []; 980 | for (const klass of classes) { 981 | if (UNNECESSARY_WARNINGS.has(klass)) { 982 | tags.push(vscode.DiagnosticTag.Unnecessary); 983 | } 984 | if (DEPRECATED_WARNINGS.has(klass)) { 985 | tags.push(vscode.DiagnosticTag.Deprecated); 986 | } 987 | } 988 | return tags; 989 | } 990 | 991 | function oldMessageToDiagnostic(message: OldMessage): vscode.Diagnostic { 992 | const range = oldMessageSpanToRange(message.span || DEFAULT_OLD_MESSAGE_SPAN); 993 | 994 | let severity = vscode.DiagnosticSeverity.Information; 995 | if (message.severity) { 996 | severity = oldMessageSeverityToDiagnostic(message.severity); 997 | } else if (message.messageClass) { 998 | severity = messageClassToDiagnosticSeverity(message.messageClass); 999 | } 1000 | 1001 | const classes = (message.reason || message.messageClass || "").split(/ +/); 1002 | 1003 | const diagnostic = new vscode.Diagnostic(range, message.doc, severity); 1004 | diagnostic.code = makeDiagnosticCode(classes); 1005 | diagnostic.source = "ghc"; 1006 | diagnostic.tags = makeDiagnosticTags(classes); 1007 | return diagnostic; 1008 | } 1009 | 1010 | function newMessageToDiagnostic(message: NewMessage): vscode.Diagnostic { 1011 | const range = newMessageSpanToRange(message.span || DEFAULT_NEW_MESSAGE_SPAN); 1012 | 1013 | let severity = vscode.DiagnosticSeverity.Information; 1014 | if (message.severity) { 1015 | severity = newMessageSeverityToDiagnostic(message.severity); 1016 | } 1017 | 1018 | const classes = message.reason 1019 | ? ('flags' in message.reason 1020 | ? message.reason.flags 1021 | : [message.reason.category]) 1022 | : []; 1023 | if (message.code) { 1024 | classes.push(`GHC-${message.code}`); 1025 | } 1026 | 1027 | const diagnostic = new vscode.Diagnostic(range, message.message.join("\n"), severity); 1028 | diagnostic.code = makeDiagnosticCode(classes); 1029 | diagnostic.source = "ghc"; 1030 | diagnostic.tags = makeDiagnosticTags(classes); 1031 | return diagnostic; 1032 | } 1033 | 1034 | function newKey(): Key { 1035 | return Math.floor(Math.random() * (0xffff + 1)) 1036 | .toString(16) 1037 | .padStart(4, "0"); 1038 | } 1039 | 1040 | async function reloadInterpreter( 1041 | channel: vscode.OutputChannel, 1042 | status: vscode.LanguageStatusItem, 1043 | collection: vscode.DiagnosticCollection 1044 | ): Promise { 1045 | const key = newKey(); 1046 | const start = perfHooks.performance.now(); 1047 | log(channel, key, "Reloading interpreter ..."); 1048 | 1049 | const document = vscode.window.activeTextEditor?.document; 1050 | if (!INTERPRETER && document) { 1051 | await startInterpreter(channel, status, collection, document); 1052 | } 1053 | if (!INTERPRETER) { 1054 | log(channel, key, "Error: Missing interpreter!"); 1055 | return; 1056 | } 1057 | 1058 | if (INTERPRETER.key) { 1059 | log(channel, key, `Ignoring because ${INTERPRETER.key} is running.`); 1060 | return; 1061 | } 1062 | 1063 | INTERPRETER.key = key; 1064 | 1065 | updateStatus(status, true, vscode.LanguageStatusSeverity.Information, "Loading"); 1066 | 1067 | if (document) { 1068 | const rootUri = getRootUri(document.uri); 1069 | collection.delete(rootUri); 1070 | } 1071 | 1072 | const input = ":reload"; 1073 | log(channel, key, `[stdin] ${input}`); 1074 | INTERPRETER.task.stdin?.write(`${input}\n`); 1075 | 1076 | while (INTERPRETER.key === key) { 1077 | await new Promise((resolve) => setTimeout(resolve, 100)); 1078 | } 1079 | 1080 | const end = perfHooks.performance.now(); 1081 | const elapsed = ((end - start) / 1000).toFixed(3); 1082 | log(channel, key, `Finished reloading in ${elapsed} seconds.`); 1083 | } 1084 | 1085 | // If the given string is an absolute path, then it is returned as a file URI. 1086 | // Otherwise the relative segment is joined with the root URI. 1087 | // 1088 | // This is necessary because Cabal uses relative paths and Stack uses absolute 1089 | // ones. See the following issues for more details: 1090 | // 1091 | // - 1092 | // - 1093 | function toAbsoluteUri(root: vscode.Uri, segment: string): vscode.Uri { 1094 | // 1095 | const normalized = path.normalize(segment); 1096 | 1097 | if (path.isAbsolute(normalized)) { 1098 | return vscode.Uri.file(normalized) 1099 | } 1100 | return vscode.Uri.joinPath(root, normalized); 1101 | } 1102 | 1103 | async function startInterpreter( 1104 | channel: vscode.OutputChannel, 1105 | status: vscode.LanguageStatusItem, 1106 | collection: vscode.DiagnosticCollection, 1107 | document: vscode.TextDocument 1108 | ): Promise { 1109 | const key = newKey(); 1110 | const start = perfHooks.performance.now(); 1111 | log(channel, key, "Starting interpreter ..."); 1112 | 1113 | const rootUri = getRootUri(document.uri); 1114 | 1115 | if (!INTERPRETER_TEMPLATE) { 1116 | log(channel, key, "Error: Missing interpreter command!"); 1117 | return; 1118 | } 1119 | 1120 | const file = vscode.workspace.asRelativePath(document.uri); 1121 | const command = expandTemplate(INTERPRETER_TEMPLATE, { file }); 1122 | 1123 | if (INTERPRETER) { 1124 | log(channel, key, `Stopping interpreter ${INTERPRETER.task.pid} ...`); 1125 | INTERPRETER.task.kill(); 1126 | INTERPRETER = null; 1127 | collection.clear(); 1128 | } 1129 | 1130 | updateStatus(status, true, vscode.LanguageStatusSeverity.Information, "Starting"); 1131 | 1132 | const cwd = rootUri.fsPath; 1133 | log( 1134 | channel, 1135 | key, 1136 | `Running ${JSON.stringify(command)} in ${JSON.stringify(cwd)} ...` 1137 | ); 1138 | const task: childProcess.ChildProcess = childProcess.spawn(command, { 1139 | cwd, 1140 | shell: true, 1141 | }); 1142 | INTERPRETER = { key, task }; 1143 | 1144 | task.on("close", (code) => { 1145 | log(channel, key, `Error: Interpreter exited with ${code}!`); 1146 | if (code !== null) { 1147 | updateStatus(status, false, vscode.LanguageStatusSeverity.Error, "Exited"); 1148 | } 1149 | }); 1150 | 1151 | assert.ok(task.stderr); 1152 | readline.createInterface(task.stderr).on("line", (line) => { 1153 | let shouldLog: boolean = true; 1154 | 1155 | let message: NewMessage | null = null; 1156 | try { 1157 | message = JSON.parse(line); 1158 | } catch (error) { 1159 | if (!(error instanceof SyntaxError)) { 1160 | throw error; 1161 | } 1162 | } 1163 | 1164 | if (message) { 1165 | let uri: vscode.Uri | null = null; 1166 | if (message.span) { 1167 | if (message.span.file !== DEFAULT_NEW_MESSAGE_SPAN.file) { 1168 | uri = toAbsoluteUri(rootUri, message.span.file); 1169 | } 1170 | } else { 1171 | uri = rootUri; 1172 | } 1173 | 1174 | if (uri) { 1175 | const diagnostic = newMessageToDiagnostic(message); 1176 | collection.set(uri, (collection.get(uri) || []).concat(diagnostic)); 1177 | 1178 | shouldLog = false; 1179 | } 1180 | } 1181 | 1182 | if (shouldLog) { 1183 | log(channel, INTERPRETER?.key || "0000", `[stderr] ${line}`); 1184 | } 1185 | }); 1186 | 1187 | const prompt = `{- ${my.name} ${my.version} ${key} -}`; 1188 | const input = `:set prompt "${prompt}\\n"`; 1189 | log(channel, key, `[stdin] ${input}`); 1190 | task.stdin?.write(`${input}\n`); 1191 | 1192 | await new Promise((resolve) => { 1193 | assert.ok(task.stdout); 1194 | readline.createInterface(task.stdout).on("line", (line) => { 1195 | let shouldLog: boolean = true; 1196 | 1197 | if (line.includes(prompt)) { 1198 | if (INTERPRETER?.key) { 1199 | INTERPRETER.key = null; 1200 | } 1201 | resolve(); 1202 | updateStatus(status, false, vscode.LanguageStatusSeverity.Information, "Idle"); 1203 | shouldLog = false; 1204 | } 1205 | 1206 | const lineMatch = line.match(COMPILING_PATTERN); 1207 | if (lineMatch) { 1208 | assert.ok(lineMatch[4]); 1209 | const uri = toAbsoluteUri(rootUri, lineMatch[4]); 1210 | collection.delete(uri); 1211 | shouldLog = false; 1212 | } 1213 | 1214 | let message: OldMessage | null = null; 1215 | try { 1216 | message = JSON.parse(line); 1217 | } catch (error) { 1218 | if (!(error instanceof SyntaxError)) { 1219 | throw error; 1220 | } 1221 | } 1222 | 1223 | if (message) { 1224 | const match = message.doc.match(COMPILING_PATTERN); 1225 | 1226 | if (match) { 1227 | assert.ok(match[4]); 1228 | const uri = toAbsoluteUri(rootUri, match[4]); 1229 | collection.delete(uri); 1230 | shouldLog = false; 1231 | } else { 1232 | let uri: vscode.Uri | null = null; 1233 | if (message.span) { 1234 | if (message.span.file !== DEFAULT_OLD_MESSAGE_SPAN.file) { 1235 | uri = toAbsoluteUri(rootUri, message.span.file); 1236 | } 1237 | } else { 1238 | uri = rootUri; 1239 | } 1240 | 1241 | if (uri) { 1242 | const diagnostic = oldMessageToDiagnostic(message); 1243 | collection.set(uri, (collection.get(uri) || []).concat(diagnostic)); 1244 | 1245 | shouldLog = false; 1246 | } 1247 | } 1248 | } 1249 | 1250 | if (shouldLog) { 1251 | log(channel, INTERPRETER?.key || "0000", `[stdout] ${line}`); 1252 | } 1253 | }); 1254 | }); 1255 | 1256 | const end = perfHooks.performance.now(); 1257 | const elapsed = ((end - start) / 1000).toFixed(3); 1258 | log(channel, key, `Successfully started in ${elapsed} seconds.`); 1259 | } 1260 | -------------------------------------------------------------------------------- /source/type/CabalFormatterMode.ts: -------------------------------------------------------------------------------- 1 | enum CabalFormatterMode { 2 | Discover = "discover", 3 | CabalFmt = "cabal-fmt", 4 | Gild = "gild", 5 | Custom = "custom", 6 | } 7 | 8 | export default CabalFormatterMode; 9 | -------------------------------------------------------------------------------- /source/type/HaskellFormatterMode.ts: -------------------------------------------------------------------------------- 1 | enum HaskellFormatterMode { 2 | Discover = "discover", 3 | Fourmolu = "fourmolu", 4 | Ormolu = "ormolu", 5 | Custom = "custom", 6 | } 7 | 8 | export default HaskellFormatterMode; 9 | -------------------------------------------------------------------------------- /source/type/HaskellLinterMode.ts: -------------------------------------------------------------------------------- 1 | enum HaskellLinterMode { 2 | Discover = "discover", 3 | Hlint = "hlint", 4 | Custom = "custom", 5 | } 6 | 7 | export default HaskellLinterMode; 8 | -------------------------------------------------------------------------------- /source/type/Idea.ts: -------------------------------------------------------------------------------- 1 | import IdeaSeverity from "./IdeaSeverity"; 2 | 3 | // https://hackage.haskell.org/package/hlint-3.6.1/docs/Language-Haskell-HLint.html#t:Idea 4 | interface Idea { 5 | decl: string[]; 6 | endColumn: number; 7 | endLine: number; 8 | file: string; 9 | from: string; 10 | hint: string; 11 | module: string[]; 12 | note: string[]; 13 | refactorings: string; 14 | severity: IdeaSeverity; 15 | startColumn: number; 16 | startLine: number; 17 | to: string | null; 18 | } 19 | 20 | export default Idea; 21 | -------------------------------------------------------------------------------- /source/type/IdeaSeverity.ts: -------------------------------------------------------------------------------- 1 | // https://hackage.haskell.org/package/hlint-3.6.1/docs/Language-Haskell-HLint.html#t:Severity 2 | enum IdeaSeverity { 3 | Ignore = "Ignore", 4 | Suggestion = "Suggestion", 5 | Warning = "Warning", 6 | Error = "Error", 7 | } 8 | 9 | export default IdeaSeverity; 10 | -------------------------------------------------------------------------------- /source/type/Interpreter.ts: -------------------------------------------------------------------------------- 1 | import childProcess from "child_process"; 2 | 3 | import Key from "./Key"; 4 | 5 | interface Interpreter { 6 | key: Key | null; 7 | task: childProcess.ChildProcess; 8 | } 9 | 10 | export default Interpreter; 11 | -------------------------------------------------------------------------------- /source/type/InterpreterMode.ts: -------------------------------------------------------------------------------- 1 | enum InterpreterMode { 2 | Discover = "discover", 3 | Cabal = "cabal", 4 | Stack = "stack", 5 | Ghci = "ghci", 6 | Custom = "custom", 7 | // Secret bonus mode! See . 8 | StackNonProject = "stack-non-project", 9 | } 10 | 11 | export default InterpreterMode; 12 | -------------------------------------------------------------------------------- /source/type/Key.ts: -------------------------------------------------------------------------------- 1 | type Key = string; 2 | 3 | export default Key; 4 | -------------------------------------------------------------------------------- /source/type/LanguageId.ts: -------------------------------------------------------------------------------- 1 | enum LanguageId { 2 | Cabal = "cabal", 3 | Haskell = "haskell", 4 | } 5 | 6 | export default LanguageId; 7 | -------------------------------------------------------------------------------- /source/type/NewMessage.ts: -------------------------------------------------------------------------------- 1 | import NewMessageReason from "./NewMessageReason"; 2 | import NewMessageSeverity from "./NewMessageSeverity"; 3 | import NewMessageSpan from "./NewMessageSpan"; 4 | 5 | // https://gitlab.haskell.org/ghc/ghc/-/blob/30bdea67fcd9755619b1f513d199f2122591b28e/docs/users_guide/diagnostics-as-json-schema-1_1.json 6 | interface NewMessage { 7 | code: number | null; 8 | message: string[]; 9 | reason: NewMessageReason | null; 10 | severity: NewMessageSeverity | null; 11 | span: NewMessageSpan | null; 12 | } 13 | 14 | export default NewMessage; 15 | -------------------------------------------------------------------------------- /source/type/NewMessageLocation.ts: -------------------------------------------------------------------------------- 1 | interface NewMessageLocation { 2 | column: number; 3 | line: number; 4 | } 5 | 6 | export default NewMessageLocation; 7 | -------------------------------------------------------------------------------- /source/type/NewMessageReason.ts: -------------------------------------------------------------------------------- 1 | type NewMessageReason 2 | = { flags: string[]; } 3 | | { category: string; }; 4 | 5 | export default NewMessageReason; 6 | -------------------------------------------------------------------------------- /source/type/NewMessageSeverity.ts: -------------------------------------------------------------------------------- 1 | enum NewMessageSeverity { 2 | Error = "Error", 3 | Warning = "Warning", 4 | } 5 | 6 | export default NewMessageSeverity; 7 | -------------------------------------------------------------------------------- /source/type/NewMessageSpan.ts: -------------------------------------------------------------------------------- 1 | import NewMessageLocation from "./NewMessageLocation"; 2 | 3 | interface NewMessageSpan { 4 | end: NewMessageLocation; 5 | file: string; 6 | start: NewMessageLocation; 7 | } 8 | 9 | export default NewMessageSpan; 10 | -------------------------------------------------------------------------------- /source/type/OldMessage.ts: -------------------------------------------------------------------------------- 1 | import OldMessageReason from "./OldMessageReason"; 2 | import OldMessageSeverity from "./OldMessageSeverity"; 3 | import OldMessageSpan from "./OldMessageSpan"; 4 | 5 | interface OldMessage { 6 | doc: string; 7 | messageClass: string | null; // Used by GHC >= 9.4. 8 | reason: OldMessageReason | null; 9 | severity: OldMessageSeverity | null; // Used by GHC < 9.4. 10 | span: OldMessageSpan | null; 11 | } 12 | 13 | export default OldMessage; 14 | -------------------------------------------------------------------------------- /source/type/OldMessageReason.ts: -------------------------------------------------------------------------------- 1 | // https://downloads.haskell.org/ghc/9.6.2/docs/libraries/ghc-9.6.2/GHC-Driver-Flags.html#t:WarningFlag 2 | type OldMessageReason = string; 3 | 4 | export default OldMessageReason; 5 | -------------------------------------------------------------------------------- /source/type/OldMessageSeverity.ts: -------------------------------------------------------------------------------- 1 | // https://downloads.haskell.org/ghc/9.6.2/docs/libraries/ghc-9.6.2/GHC-Types-Error.html#t:Severity 2 | enum OldMessageSeverity { 3 | SevDump = "SevDump", 4 | SevError = "SevError", 5 | SevFatal = "SevFatal", 6 | SevInfo = "SevInfo", 7 | SevInteractive = "SevInteractive", 8 | SevOutput = "SevOutput", 9 | SevWarning = "SevWarning", 10 | } 11 | 12 | export default OldMessageSeverity; 13 | -------------------------------------------------------------------------------- /source/type/OldMessageSpan.ts: -------------------------------------------------------------------------------- 1 | // https://downloads.haskell.org/ghc/9.6.2/docs/libraries/ghc-9.6.2/GHC-Types-SrcLoc.html#t:SrcSpan 2 | interface OldMessageSpan { 3 | endCol: number; 4 | endLine: number; 5 | file: string; // Can be `""`. 6 | startCol: number; 7 | startLine: number; 8 | } 9 | 10 | export default OldMessageSpan; 11 | -------------------------------------------------------------------------------- /source/type/Template.ts: -------------------------------------------------------------------------------- 1 | type Template = string; 2 | 3 | export default Template; 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "resolveJsonModule": true 5 | }, 6 | "extends": [ 7 | "@tsconfig/node20/tsconfig.json", 8 | "@tsconfig/strictest/tsconfig.json" 9 | ] 10 | } 11 | --------------------------------------------------------------------------------