├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── package.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode-test.js ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── .yarnclean ├── Changelog.md ├── GenChangelogs.hs ├── LICENSE ├── README.md ├── default.nix ├── docs ├── Contributing.md └── Release.md ├── eslint.config.mjs ├── images └── hls-logo.png ├── package.json ├── src ├── commands │ └── constants.ts ├── config.ts ├── docsBrowser.ts ├── errors.ts ├── extension.ts ├── ghcup.ts ├── hlsBinaries.ts ├── logger.ts ├── metadata.ts ├── statusBar.ts └── utils.ts ├── test └── suite │ ├── extension.test.ts │ └── index.ts ├── tsconfig.json ├── webpack.config.js └── yarn.lock /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: 'status: needs triage' 6 | assignees: '' 7 | --- 8 | 9 | 14 | 15 | ### Your environment 16 | 17 | Which OS do you use: 18 | 19 | 20 | 21 | ### Steps to reproduce 22 | 23 | 24 | 25 | ### Expected behaviour 26 | 27 | 28 | 29 | ### Actual behaviour 30 | 31 | 32 | 33 | ### Include debug information 34 | 35 | Execute in the root of your project the command `haskell-language-server-wrapper --debug .` and paste the logs here (you can find the executable location [here](https://github.com/haskell/vscode-haskell#downloaded-binaries)): 36 | 37 |
38 | 39 | Debug output: 40 | 41 | 42 | ``` 43 | 44 | ``` 45 | 46 |
47 | 48 | Paste the contents of extension specific log, you can check instructions about how to find it [here](https://github.com/haskell/vscode-haskell#troubleshooting) 49 | 50 |
51 | 52 | Extension log: 53 | 54 | 55 | ``` 56 | 57 | ``` 58 | 59 |
60 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | 11 | 12 | 13 | **Describe the solution you'd like** 14 | 15 | 16 | 17 | **Describe alternatives you've considered** 18 | 19 | 20 | 21 | **Additional context** 22 | 23 | 24 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # NOTE: Dependabot official configuration documentation: 4 | # https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates#package-ecosystem 5 | 6 | # Maintain dependencies for internal GitHub Actions CI for pull requests 7 | - package-ecosystem: 'github-actions' 8 | directory: '/' 9 | schedule: 10 | interval: 'weekly' 11 | 12 | - package-ecosystem: 'npm' 13 | directory: '/' 14 | schedule: 15 | interval: 'weekly' 16 | -------------------------------------------------------------------------------- /.github/workflows/package.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request: 6 | branches: 7 | - '**' 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | os: [ubuntu-latest] 15 | runs-on: ${{ matrix.os }} 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v5 19 | 20 | - name: Install Node.js 21 | uses: actions/setup-node@v5 22 | with: 23 | ## make sure this corresponds with the version in release.yml 24 | node-version: latest 25 | 26 | - run: yarn install --immutable --immutable-cache --check-cache 27 | 28 | - name: Package extension 29 | run: npx vsce package 30 | - name: Upload extension vsix to workflow artifacts 31 | uses: actions/upload-artifact@v4 32 | with: 33 | name: haskell-${{ github.sha }}.vsix 34 | path: haskell-*.vsix 35 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: [prereleased, released] 4 | 5 | name: Deploy Extension 6 | jobs: 7 | publish-extension: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v5 11 | - uses: actions/setup-node@v5 12 | with: 13 | ## make sure this corresponds with the version in test.yml 14 | node-version: latest 15 | - run: yarn install --immutable --immutable-cache --check-cache 16 | 17 | - name: Package Extension 18 | id: packageExtension 19 | uses: HaaLeo/publish-vscode-extension@v2 20 | with: 21 | pat: stub 22 | dryRun: true 23 | preRelease: ${{ github.event.action == 'prereleased' }} 24 | yarn: true 25 | 26 | ## Make sure the artifact is added to the release. 27 | - name: Upload extension vsix to workflow artifacts 28 | uses: actions/upload-artifact@v4 29 | with: 30 | name: haskell-${{ github.event.release.tag_name }}.vsix 31 | path: ${{ steps.packageExtension.outputs.vsixPath }} 32 | 33 | ## If this is a release job, publish to VSCode Marketplace, 34 | ## otherwise publish a pre-release to VSCode Marketplace 35 | - name: Publish to Visual Studio Marketplace 36 | id: publishToVSMarketplace 37 | uses: HaaLeo/publish-vscode-extension@v2 38 | with: 39 | pat: ${{ secrets.VS_MARKETPLACE_TOKEN }} 40 | registryUrl: https://marketplace.visualstudio.com 41 | extensionFile: ${{ steps.packageExtension.outputs.vsixPath }} 42 | yarn: true 43 | preRelease: ${{ github.event.action == 'prereleased' }} 44 | 45 | ## If this is a release job, publish to VSX Marketplace, 46 | ## otherwise publish a pre-release to VSX Marketplace 47 | - name: Publish to Open VSX Registry 48 | id: publishToOpenVSX 49 | continue-on-error: true 50 | uses: HaaLeo/publish-vscode-extension@v2 51 | with: 52 | pat: ${{ secrets.OPEN_VSX_TOKEN }} 53 | extensionFile: ${{ steps.packageExtension.outputs.vsixPath }} 54 | yarn: true 55 | preRelease: ${{ github.event.action == 'prereleased' }} 56 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request: 6 | branches: 7 | - '**' 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | os: [macos-latest, ubuntu-latest, windows-latest] 15 | ghc: [8.10.7, 9.6.7, 9.8.4, 9.12.2] 16 | runs-on: ${{ matrix.os }} 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v5 20 | 21 | - name: Install Node.js 22 | uses: actions/setup-node@v5 23 | with: 24 | ## make sure this corresponds with the version in release.yml 25 | node-version: latest 26 | 27 | # Install test dependencies 28 | - run: yarn install --immutable --immutable-cache --check-cache 29 | - run: yarn run webpack 30 | 31 | # Setup toolchains, install ghcup, install ghc, etc... 32 | - name: Install GHCup 33 | run: | 34 | curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh 35 | shell: bash 36 | env: 37 | BOOTSTRAP_HASKELL_NONINTERACTIVE: 1 38 | BOOTSTRAP_HASKELL_MINIMAL: 1 39 | 40 | - name: Check GHCup (Windows) 41 | run: | 42 | echo "c:/ghcup/bin" >> $GITHUB_PATH 43 | shell: bash 44 | if: runner.os == 'Windows' 45 | 46 | - name: Check GHCup (Unix) 47 | run: | 48 | echo "${HOME}/.ghcup/bin" >> $GITHUB_PATH 49 | shell: bash 50 | if: runner.os != 'Windows' 51 | 52 | - name: Toolchain settings 53 | run: | 54 | ghcup upgrade -i -f 55 | export GHCUP_INSTALL_BASE_PREFIX=$(pwd)/test-workspace/bin 56 | ghcup config set cache true 57 | 58 | ghcup install stack latest 59 | ghcup install cabal latest 60 | 61 | ghcup install ghc ${{ matrix.ghc }} 62 | ghcup set ghc ${{ matrix.ghc }} 63 | 64 | # This is a prefetched, fallback HLS version. 65 | # We want to make sure, we still support old GHC versions 66 | # and graciously fallback to an HLS version that supports the old GHC version, such as 8.10.7 67 | ghcup install hls 2.2.0.0 68 | ghcup install hls latest 69 | shell: bash 70 | 71 | # Run the tests 72 | - name: Run the test on Linux 73 | run: | 74 | export GHCUP_INSTALL_BASE_PREFIX=$(pwd)/test-workspace/bin 75 | export PATH="$(pwd)/test-workspace/bin/.ghcup/bin:$PATH" 76 | xvfb-run -s '-screen 0 640x480x16' -a yarn run test 77 | shell: bash 78 | if: runner.os == 'Linux' 79 | - name: Run the test on macOS 80 | run: | 81 | export GHCUP_INSTALL_BASE_PREFIX=$(pwd)/test-workspace/bin 82 | export PATH="$(pwd)/test-workspace/bin/.ghcup/bin:$PATH" 83 | yarn run test 84 | shell: bash 85 | if: runner.os == 'macOS' 86 | - name: Run the test on Windows 87 | run: | 88 | export GHCUP_INSTALL_BASE_PREFIX=$(pwd)/test-workspace/bin 89 | export PATH="$(pwd)/test-workspace/bin/ghcup/bin:$PATH" 90 | yarn run test 91 | shell: bash 92 | if: runner.os == 'Windows' 93 | 94 | # Create package artefacts 95 | - name: Delete test artefacts 96 | # The test-suite doesn't clean up correctly after itself. 97 | # This is a poor man's workaround that after test execution, 98 | # the test-workspace still contains binaries and caches. 99 | run: | 100 | rm -rf test-workspace 101 | rm -rf out 102 | shell: bash 103 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | .vscode-test 4 | test-workspace 5 | .DS_Store 6 | dist 7 | *.vsix 8 | .husky -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | test-workspace/ 3 | .vscode/ 4 | .vscode-test/ 5 | out/ 6 | dist/ 7 | webpack.config.js 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | printWidth: 120 2 | singleQuote: true 3 | -------------------------------------------------------------------------------- /.vscode-test.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require('@vscode/test-cli'); 2 | 3 | module.exports = defineConfig([ 4 | { 5 | label: 'integration-tests', 6 | files: 'out/test/**/*.test.js', 7 | version: 'stable', 8 | workspaceFolder: './test-workspace', 9 | installExtensions: ['justusadam.language-haskell'], 10 | mocha: { 11 | timeout: 120 * 1000, // 2 minute timeout 12 | }, 13 | }, 14 | // you can specify additional test configurations, too 15 | ]); 16 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", 4 | "esbenp.prettier-vscode" 5 | ] 6 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | { 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "name": "Extension", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": ["--extensionDevelopmentPath=${workspaceRoot}"], 11 | "sourceMaps": true, 12 | "outFiles": ["${workspaceRoot}/dist/**/*.js"], 13 | "preLaunchTask": "npm: webpack" 14 | }, 15 | { 16 | "name": "Extension Tests", 17 | "type": "extensionHost", 18 | "request": "launch", 19 | "runtimeExecutable": "${execPath}", 20 | "testConfiguration": "${workspaceFolder}/.vscode-test.js", 21 | "args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/out/test"], 22 | "sourceMaps": true, 23 | "outFiles": ["${workspaceRoot}/out/test/**/*.js"], 24 | "preLaunchTask": "npm: pretest" 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "dist": true, // set this to true to hide the "dist" folder with the compiled JS files 5 | ".vscode-test": true, 6 | "node_modules": true 7 | }, 8 | "search.exclude": { 9 | "dist": true // set this to false to include "dist" folder in search results 10 | }, 11 | "typescript.tsdk": "./node_modules/typescript/lib", 12 | "editor.tabSize": 2, // we want to use the TS server from our node_modules folder to control its version, 13 | "editor.formatOnSave": true, 14 | "files.associations": { 15 | ".prettierrc": "yaml" 16 | }, 17 | "files.eol": "\n", 18 | "haskell.formattingProvider": "stylish-haskell" 19 | } -------------------------------------------------------------------------------- /.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": { 10 | "owner": "typescript", 11 | "applyTo": "closedDocuments", 12 | "fileLocation": ["absolute"], 13 | "pattern": { 14 | "regexp": "" 15 | }, 16 | "background": { 17 | "activeOnStart": true, 18 | "beginsPattern": { 19 | "regexp": "Compilation (.*?)starting…" 20 | }, 21 | "endsPattern": { 22 | "regexp": "Compilation (.*?)finished" 23 | } 24 | } 25 | }, 26 | "isBackground": true, 27 | "presentation": { 28 | "reveal": "never" 29 | }, 30 | "group": { 31 | "kind": "build", 32 | "isDefault": true 33 | } 34 | }, 35 | { 36 | "type": "npm", 37 | "script": "test", 38 | "group": { 39 | "kind": "test", 40 | "isDefault": true 41 | } 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | typings/** 4 | out/test/** 5 | test/** 6 | src/** 7 | **/*.map 8 | .gitignore 9 | tsconfig.json 10 | node_modules 11 | out 12 | src 13 | webpack.config.json 14 | -------------------------------------------------------------------------------- /.yarnclean: -------------------------------------------------------------------------------- 1 | # test directories 2 | __tests__ 3 | test 4 | tests 5 | powered-test 6 | 7 | # asset directories 8 | docs 9 | doc 10 | website 11 | images 12 | assets 13 | 14 | # examples 15 | example 16 | examples 17 | 18 | # code coverage directories 19 | coverage 20 | .nyc_output 21 | 22 | # build scripts 23 | Makefile 24 | Gulpfile.js 25 | Gruntfile.js 26 | 27 | # configs 28 | appveyor.yml 29 | circle.yml 30 | codeship-services.yml 31 | codeship-steps.yml 32 | wercker.yml 33 | .tern-project 34 | .gitattributes 35 | .editorconfig 36 | .*ignore 37 | .eslintrc 38 | .jshintrc 39 | .flowconfig 40 | .documentup.json 41 | .yarn-metadata.json 42 | .travis.yml 43 | 44 | # misc 45 | *.md 46 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog for vscode-haskell 2 | 3 | ## 2.7.0 4 | 5 | - Introduce the `StatusBarItem` 6 | ([#1237](https://github.com/haskell/vscode-haskell/pull/1237)) by @fendor 7 | 8 | ## 2.6.1 9 | 10 | - Prefer the `set` version for `cabal` and `stack` if there is any 11 | ([#1275](https://github.com/haskell/vscode-haskell/pull/1275)) by @fendor 12 | - Make js debugger work 13 | ([#1258](https://github.com/haskell/vscode-haskell/pull/1258)) by @dyniec 14 | - Prepare release 2.6.0 15 | ([#1103](https://github.com/haskell/vscode-haskell/pull/1103)) by @fendor 16 | 17 | ## 2.6.0 18 | 19 | - Add option to enable/disable `.cabal` file support 20 | ([#1223](https://github.com/haskell/vscode-haskell/pull/1223)) by @fendor 21 | - Upgrade project to use latest eslint version 22 | ([#1150](https://github.com/haskell/vscode-haskell/pull/1150)) by @fendor 23 | - Fix windows CI 24 | ([#1149](https://github.com/haskell/vscode-haskell/pull/1149)) by @fendor 25 | - Manually install ghcup into image 26 | ([#1119](https://github.com/haskell/vscode-haskell/pull/1119)) by @fendor 27 | - bump vscode-languageclient version to 9.0.1 28 | ([#1108](https://github.com/haskell/vscode-haskell/pull/1108)) by @jetjinser 29 | - Add cabalFormattingProvider to package.json 30 | ([#1100](https://github.com/haskell/vscode-haskell/pull/1100)) by @fendor 31 | 32 | ## 2.5.3 33 | 34 | - Split out packaging action 35 | ([#1080](https://github.com/haskell/vscode-haskell/pull/1080)) by @fendor 36 | - Add Session Loading style to list of known configs 37 | ([#1077](https://github.com/haskell/vscode-haskell/pull/1077)) by @fendor 38 | - Tooling update 39 | ([#1043](https://github.com/haskell/vscode-haskell/pull/1043)) by @bzm3r 40 | - Add `haskell.plugin.fourmolu.config.path` option 41 | ([#987](https://github.com/haskell/vscode-haskell/pull/987)) by @georgefst 42 | 43 | ## 2.5.2 44 | 45 | - Includes changes of the 2.4.3 release 46 | 47 | ## 2.5.1 48 | 49 | - Includes changes of the 2.4.2 release 50 | 51 | ## 2.5.0 52 | 53 | - Add tracking of cabal files 54 | ([#618](https://github.com/haskell/vscode-haskell/pull/618)) by @fendor 55 | 56 | ## 2.4.3 57 | 58 | - Address invalid byte sequence error #1022 59 | ([#1022](https://github.com/haskell/vscode-haskell/pull/1022)) by @felixlinker 60 | - Always set the cwd for the executable (#1011) 61 | ([#1011](https://github.com/haskell/vscode-haskell/pull/1011)) by @fendor 62 | 63 | ## 2.4.2 64 | 65 | - Add stan plugin option #1000 66 | ([#1000](https://github.com/haskell/vscode-haskell/pull/1000)) by @fendor 67 | - Probe for GHCup binary wrt #962 68 | ([#963](https://github.com/haskell/vscode-haskell/pull/963)) by @hasufell 69 | - Bump old hls version and upgrade test runner to macos-latest 70 | ([#960](https://github.com/haskell/vscode-haskell/pull/960)) by @July541 71 | - Increase time limitation to make test on Windows more stable 72 | ([#959](https://github.com/haskell/vscode-haskell/pull/959)) by @July541 73 | - Update release docs for refreshing CI tokens 74 | ([#942](https://github.com/haskell/vscode-haskell/pull/942)) by @fendor 75 | 76 | ## 2.4.1 77 | 78 | - Downgrade vscode-languageclient 79 | ([#934](https://github.com/haskell/vscode-haskell/pull/934)) by @fendor 80 | - Bump vscode to 1.80.0 81 | ([#912](https://github.com/haskell/vscode-haskell/pull/912)) by @July541 82 | 83 | ## 2.4.0 84 | 85 | - Prepare release 2.4.0 86 | ([#906](https://github.com/haskell/vscode-haskell/pull/906)) by @VeryMilkyJoe 87 | - Simplify tests 88 | ([#904](https://github.com/haskell/vscode-haskell/pull/904)) by @July541 89 | - Remove unused code 90 | ([#898](https://github.com/haskell/vscode-haskell/pull/898)) by @fendor 91 | - Remove hoogle command from vscode extension 92 | ([#896](https://github.com/haskell/vscode-haskell/pull/896)) by @fendor 93 | - Update readme 94 | ([#886](https://github.com/haskell/vscode-haskell/pull/886)) by @VeryMilkyJoe 95 | - Fix broken tests 96 | ([#880](https://github.com/haskell/vscode-haskell/pull/880)) by @July541 97 | - Update README.md: clarify how to use Stack with vscode-haskell extension 98 | ([#874](https://github.com/haskell/vscode-haskell/pull/874)) by @miguel-negrao 99 | - Remove debugger tools from CI 100 | ([#873](https://github.com/haskell/vscode-haskell/pull/873)) by @fendor 101 | - Refactor tests to work correctly 102 | ([#872](https://github.com/haskell/vscode-haskell/pull/872)) by @July541 103 | - Downgrade vscode language client to 7.0.0 104 | ([#853](https://github.com/haskell/vscode-haskell/pull/853)) by @fendor 105 | - Update badge url for VSCode Marketplace 106 | ([#851](https://github.com/haskell/vscode-haskell/pull/851)) by @fendor 107 | 108 | ## 2.2.4 109 | 110 | - Downgrade vscode language client to 7.0.0 111 | ([#843](https://github.com/haskell/vscode-haskell/pull/853)) by @fendor 112 | 113 | ## 2.2.3 114 | 115 | - Prepare release 2.2.3 116 | ([#843](https://github.com/haskell/vscode-haskell/pull/843)) by @fendor 117 | - Add new plugins fields 118 | ([#842](https://github.com/haskell/vscode-haskell/pull/842)) by @fendor 119 | - Migrate to eslint 120 | ([#782](https://github.com/haskell/vscode-haskell/pull/782)) by @fendor 121 | - Bump minor versions of package dependencies 122 | ([#781](https://github.com/haskell/vscode-haskell/pull/781)) by @fendor 123 | - Update unsupported GHC doc link 124 | ([#776](https://github.com/haskell/vscode-haskell/pull/776)) by @limaak 125 | - Fix release CI 126 | ([#775](https://github.com/haskell/vscode-haskell/pull/775)) by @fendor 127 | - Fix mistake in generated ChangeLog 128 | ([#774](https://github.com/haskell/vscode-haskell/pull/774)) by @fendor 129 | 130 | ## 2.2.2 131 | 132 | - Add link to HLS installation webpage 133 | ([#751](https://github.com/haskell/vscode-haskell/pull/751)) by @fendor 134 | - Change scope of serverExecutablePath to machine-overridable 135 | ([#742](https://github.com/haskell/vscode-haskell/pull/742)) by @fendor 136 | - Add Fourmolu config property 137 | ([#736](https://github.com/haskell/vscode-haskell/pull/736)) by @georgefst 138 | - Add missing configuration options for the latest HLS version 139 | ([#717](https://github.com/haskell/vscode-haskell/pull/717)) by @fendor 140 | - Change sensible to sensitive 141 | ([#709](https://github.com/haskell/vscode-haskell/pull/709)) by @ploeh 142 | 143 | ## 2.2.1 144 | 145 | - Fix test-suite for new GHCUp release 146 | ([#672](https://github.com/haskell/vscode-haskell/pull/672)) by @fendor 147 | - Bump webpack from 5.73.0 to 5.74.0 148 | ([#657](https://github.com/haskell/vscode-haskell/pull/657)) by @fendor 149 | - Bump typescript from 4.4.0 to 4.7.4 150 | ([#657](https://github.com/haskell/vscode-haskell/pull/657)) by @fendor 151 | - Bump @types/node from 18.0.4 to 18.6.1 152 | ([#657](https://github.com/haskell/vscode-haskell/pull/657)) by @fendor 153 | - Bump @typescript-eslint/eslint-plugin from 5.30.6 to 5.31.0 154 | ([#657](https://github.com/haskell/vscode-haskell/pull/657)) by @fendor 155 | - Bump @typescript-eslint/parser from 5.30.6 to 5.31.0 156 | ([#657](https://github.com/haskell/vscode-haskell/pull/657)) by @fendor 157 | - Bump prettier from 2.6.2 to 2.7.1 158 | ([#657](https://github.com/haskell/vscode-haskell/pull/657)) by @fendor 159 | - Bump mocha from 9.2.1 to 10.0.0 160 | ([#657](https://github.com/haskell/vscode-haskell/pull/657)) by @fendor 161 | - Add dependabot.yml 162 | ([#633](https://github.com/haskell/vscode-haskell/pull/633)) by @fendor 163 | - Replace x32 with ia32 for Architecture matching 164 | ([#631](https://github.com/haskell/vscode-haskell/pull/631)) by @fendor 165 | - Toolchain management dialog: add hint for beginners 166 | ([#621](https://github.com/haskell/vscode-haskell/pull/621)) by @runeksvendsen 167 | - Fix trace.server option 168 | ([#617](https://github.com/haskell/vscode-haskell/pull/617)) by @coltenwebb 169 | - Add TOC 170 | ([#615](https://github.com/haskell/vscode-haskell/pull/615)) by @hasufell 171 | - Cleanups 172 | ([#605](https://github.com/haskell/vscode-haskell/pull/605)) by @hasufell 173 | - Link to VSCode settings page 174 | ([#603](https://github.com/haskell/vscode-haskell/pull/603)) by @hasufell 175 | - Refactor toInstall shenanigans 176 | ([#600](https://github.com/haskell/vscode-haskell/pull/600)) by @hasufell 177 | - Fix confusing download dialog popup 178 | ([#599](https://github.com/haskell/vscode-haskell/pull/599)) by @hasufell 179 | - More troubleshooting 180 | ([#598](https://github.com/haskell/vscode-haskell/pull/598)) by @hasufell 181 | 182 | ## 2.2.0 183 | 184 | - Bump version to 2.2.0 (Syncs up pre-release and release version) 185 | ([#594](https://github.com/haskell/vscode-haskell/pull/594)) by @fendor 186 | 187 | ## 2.0.1 188 | 189 | - Bad error message when ghcup is not installed 190 | ([#591](https://github.com/haskell/vscode-haskell/pull/591)) by @hasufell 191 | - Better error message if we can't find a HLS version for a given GHC 192 | ([#588](https://github.com/haskell/vscode-haskell/pull/588)) by @hasufell 193 | - Properly convert release metadata from json 194 | ([#585](https://github.com/haskell/vscode-haskell/pull/585)) by @fendor 195 | - Ignore missing entries in Release Metadata 196 | ([#582](https://github.com/haskell/vscode-haskell/pull/582)) by @fendor 197 | - Add Tool class and print stacktraces 198 | ([#579](https://github.com/haskell/vscode-haskell/pull/579)) by @fendor 199 | - List Env Vars we care about only 200 | ([#578](https://github.com/haskell/vscode-haskell/pull/578)) by @fendor 201 | - Prepare pre-release 2.1.0 202 | ([#574](https://github.com/haskell/vscode-haskell/pull/574)) by @fendor 203 | - Enable pre-release feature for VSX Marketplace 204 | ([#573](https://github.com/haskell/vscode-haskell/pull/573)) by @fendor 205 | - Add prettier script 206 | ([#566](https://github.com/haskell/vscode-haskell/pull/566)) by @fendor 207 | - Remove accidental run command 208 | ([#565](https://github.com/haskell/vscode-haskell/pull/565)) by @fendor 209 | - Upgrade dependencies 210 | ([#564](https://github.com/haskell/vscode-haskell/pull/564)) by @fendor 211 | - Add new configuration options for rename plugin 212 | ([#563](https://github.com/haskell/vscode-haskell/pull/563)) by @OliverMadine 213 | - Introduce 'haskell.toolchain' setting 214 | ([#562](https://github.com/haskell/vscode-haskell/pull/562)) by @hasufell 215 | - Improve 216 | ([#558](https://github.com/haskell/vscode-haskell/pull/558)) by @hasufell 217 | - Remove stdout/sterr from user error message 218 | ([#556](https://github.com/haskell/vscode-haskell/pull/556)) by @fendor 219 | - Fix npm security issue 220 | ([#555](https://github.com/haskell/vscode-haskell/pull/555)) by @fendor 221 | - No colour output for GHCup 222 | ([#554](https://github.com/haskell/vscode-haskell/pull/554)) by @fendor 223 | - Add eval plugin configuration 224 | ([#549](https://github.com/haskell/vscode-haskell/pull/549)) by @xsebek 225 | - Manage all the Haskell things 226 | ([#547](https://github.com/haskell/vscode-haskell/pull/547)) by @hasufell 227 | - Consider user installed HLSes (e.g. via ghcup compile) 228 | ([#543](https://github.com/haskell/vscode-haskell/pull/543)) by @hasufell 229 | - Update README.MD GHC support 230 | ([#537](https://github.com/haskell/vscode-haskell/pull/537)) by @cptwunderlich 231 | - fix: change deprecated Haskell Platform install link to GHCup 232 | ([#536](https://github.com/haskell/vscode-haskell/pull/536)) by @HEIGE-PCloud 233 | - Update HLS installation method 234 | ([#533](https://github.com/haskell/vscode-haskell/pull/533)) by @hasufell 235 | - Fixes related with paths 236 | ([#518](https://github.com/haskell/vscode-haskell/pull/518)) by @jneira 237 | - Reorganize troubleshooting section 238 | ([#516](https://github.com/haskell/vscode-haskell/pull/516)) by @jneira 239 | 240 | ## 1.8.0 241 | 242 | This release includes some interesting new features: 243 | 244 | - You can now pass custom environment variables to the lsp server 245 | with the `haskell.serverEnvironment` config option per project basis, 246 | thanks to [@jacobprudhomme](https://github.com/jacobprudhomme). 247 | - For example: `"haskell.serverEnvironment": { "XDG_CACHE_HOME": "/path/to/my/cache" }` 248 | - With this version the extension will try to use the newer lsp server version 249 | which supports the ghc used by the project being loaded, thanks to [@mduerig](https://github.com/mduerig) 250 | - WARNING: This will suppose it will use an older version than the latest one, 251 | without its features and bug fixes. 252 | - The extension has lot of more log traces now, which hopefully will help to 253 | identify the cause of issues 254 | 255 | ### Pull requests merged for 1.8.0 256 | 257 | - Update supported ghc versions for hls-1.5.1 258 | ([#514](https://github.com/haskell/vscode-haskell/pull/514)) by @jneira 259 | - Fix hole_severity option: Use integer instead of string 260 | ([#511](https://github.com/haskell/vscode-haskell/pull/511)) by @mirko-plowtech 261 | - Update issue templates 262 | ([#509](https://github.com/haskell/vscode-haskell/pull/509)) by @jneira 263 | - Add traces for download hls 264 | ([#508](https://github.com/haskell/vscode-haskell/pull/508)) by @jneira 265 | - support old hls versions compatible with the requested ghc version 266 | ([#506](https://github.com/haskell/vscode-haskell/pull/506)) by @mduerig 267 | - Fix ci: ensure we have a supported ghc version in PATH 268 | ([#496](https://github.com/haskell/vscode-haskell/pull/496)) by @jneira 269 | - Trace environment variables 270 | ([#495](https://github.com/haskell/vscode-haskell/pull/495)) by @jneira 271 | - Pass environment variables to LSP 272 | ([#494](https://github.com/haskell/vscode-haskell/pull/494)) by @jacobprudhomme 273 | - Reorganize README 274 | ([#491](https://github.com/haskell/vscode-haskell/pull/491)) by @jneira 275 | - Fix error handling of server exec discovery in windows 276 | ([#486](https://github.com/haskell/vscode-haskell/pull/486)) by @jneira 277 | - Bump versions of ts, cheerio, mocha 278 | ([#485](https://github.com/haskell/vscode-haskell/pull/485)) by @jneira 279 | - Improve serverExecutablePath description and error when pointing to a directory 280 | ([#484](https://github.com/haskell/vscode-haskell/pull/484)) by @jneira 281 | - Add integration smoke test 282 | ([#481](https://github.com/haskell/vscode-haskell/pull/481)) by @jneira 283 | - Setup the test suite 284 | ([#475](https://github.com/haskell/vscode-haskell/pull/475)) by @jneira 285 | 286 | ## 1.7.1 287 | 288 | - Bug fix release due to #471 and fixed with #469 thanks to [@berberman](https://github.com/berberman) 289 | 290 | ## 1.7.0 291 | 292 | - Add an option to set server command line arguments thanks to [@cdsmith](https://github.com/cdsmith) 293 | - It includes a new config option `haskell.serverExtraArgs` to being able to pass extra argument to the lsp server executable 294 | - Update config options to match last haskell-language-server version 295 | - It removes `haskell.diagnosticsOnChange` and `haskell.formatOnImportOn` cause they were unused in the server 296 | - It adds `haskell.checkProject`, `haskell.maxCompletions` and `haskell.plugin.refineImports.globalOn` 297 | - Fix showDocumentation command thanks to [@pranaysashank](https://github.com/pranaysashank) 298 | - It fixes partially showing the documentation directly in vscode. The documentation is rendered but internal links still does not work 299 | - Two config options has been added: `haskell.openDocumentationInHackage` and `haskell.openSourceInHackage` with default value `true` 300 | - So documentation will be opened using the hackage url in an external navigator by default 301 | - If you prefer having them in vscode you will need to change them to `false` 302 | - Create output channel only if there are no existing clients thanks to [@pranaysashank](https://github.com/pranaysashank) 303 | - This fixes the creation of several output channels for the extension 304 | 305 | ## 1.6.1 306 | 307 | - Fix wrapper call to get project ghc version in windows with spaces in path () 308 | 309 | ## 1.6.0 310 | 311 | - Bump up vscode version to 1.52.0 (#424) by [@berberman](https://github.com/berberman) 312 | - To match the lsp spec version used in haskell-language-version and fix 313 | 314 | ## 1.5.1 315 | 316 | - Add much more logging in the client side, configured with `haskell.trace.client` 317 | - Fix error handling of `working out project ghc` and a bug when the path to the executable contains spaces (See #421) 318 | - And dont use a shell to spawn the subprocess in non windows systems 319 | - Show the progress as a cancellable notification 320 | - Add commands `Start Haskell LSP server` and `Stop Haskell LSP server` 321 | 322 | ## 1.5.0 323 | 324 | - Emit warning about limited support for ghc-9.x on hls executable download 325 | - Fix `working out project ghc` progress notificacion 326 | - Fix tactics config, thanks to @isovector 327 | - Update server config to match haskell-language-server-1.3.0 one 328 | 329 | ## 1.4.0 330 | 331 | - Restore `resource` scope for `haskell.serverExecutablePath` temporary. The `machine` scope will be set again after giving users a period of time to let them adapt theirs workflows and changing or adding some option in the extension itself to help that adjustement (see #387). 332 | 333 | ## 1.3.0 334 | 335 | - Add `haskell.releasesURL` option to override where to look for HLS releases search for HLS downloads, thanks to @soiamsoNG 336 | - With this version _the only supported lsp server variant is [`haskell-language-server`](https://github.com/haskell/haskell-language-server)_ 337 | - Add support for generic plugin configuration. Thanks to it, each plugin capability (diagnostics, code actions, code lenses, etc) or the entire plugin can be disabled 338 | - Add some plugin specic options: 339 | - [wingman](https://haskellwingman.dev/) (aka tactics) plugin 340 | - `haskell.plugin.tactic.config.features`: Feature set used by the plugin 341 | - `haskell.plugin.tactics.config.hole_severity`: The severity to use when showing hole diagnostics 342 | - `haskell.plugin.tactic.config.max_use_ctor_actions`: Maximum number of `Use constructor ` code actions that can appear 343 | - `haskell.plugin.tactics.config.timeout_duration`: The timeout for Wingman actions, in seconds 344 | - completions 345 | - `haskell.plugin.ghcide-completions.config.autoExtendOn`: Extends the import list automatically when completing a out-of-scope identifier 346 | - `haskell.plugin.ghcide-completions.config.snippetsOn`: Inserts snippets when using code completions 347 | - type signature lenses - `haskell.plugin.ghcide-type-lenses.config.mode`: Control how type lenses are shown 348 | - The option `haskell.serverExecutablePath` has now `machine` scope, so it can be only changed globally by the user. It avoids a potential security vulnerability as folders containing `.vscode/settings.json` with that option could execute arbitrary programs. 349 | - Deprecated options: 350 | - `haskell.hlintOn`: use `haskell.plugin.hlint.globalOn` instead. 351 | - `haskell.completionSnippetsOn`: use `haskell.plugin.ghcide-completions.config.snippetsOn` 352 | - Fixed a small typo that caused the server not to be loaded in `.lhs` files, thanks to @Max7cd 353 | 354 | ## 1.2.0 355 | 356 | - Add option to open local documentation on Hackage (@DunetsNM) 357 | - Add `haskell.updateBehaviour` option to configure when to check for updates 358 | (@WorldSEnder) 359 | - Use locally installed servers on connection failure (@WorldSEnder) 360 | 361 | ## 1.1.0 362 | 363 | - Add Fourmolu as a plugin formatter provider (@georgefst) 364 | - Remove the `haskell.enable` configuration option, since VS Code now allows 365 | you to disable extensions on a per workspace basis 366 | - Display errors when fetching from the GitHub API properly 367 | 368 | ## 1.0.1 369 | 370 | - Switch the default formatter to Ormolu to match haskell-language-server 371 | - Fix `haskell.serverExecutablePath` not working with absolute paths on Windows 372 | (@winestone) 373 | - Improve the help text and error message when `haskell.serverExecutablePath` 374 | is not found 375 | - Fix the rendering of the markdown table in the README (@Darren8098) 376 | 377 | ## 1.0.0 378 | 379 | - vscode-haskell now lives under the Haskell organisation 380 | - Can now download zip archived binaries, which the Windows binaries are now distributed as 381 | - Improve README (@pepeiborra @jaspervdj) 382 | 383 | ## 0.1.1 384 | 385 | - Fix the restart server and import identifier commands 386 | 387 | ## 0.1.0 388 | 389 | `vscode-hie-server`/`Haskell Language Server` is now just Haskell, and will soon 390 | be published under the Haskell organisation as `haskell-vscode`. 391 | This release makes haskell-language-server the default langauge server of choice 392 | and automatically downloads and installs binaries. Installation from source is 393 | still supported though and any binaries located on your PATH for the selected 394 | langauge server will be used instead. 395 | 396 | ### Important! 397 | 398 | As part of this, your configuration may be reset as the keys move from 399 | `languageServerHaskell.completionSnippetsOn` to `haskell.completionSnippetsOn`. 400 | 401 | - Fix the document and source browser 402 | - Remove obselete commands that are no longer supported by any of the language 403 | servers 404 | - Show type command 405 | - Insert type command 406 | - HaRe commands 407 | - Case split commands 408 | 409 | ## 0.0.40 410 | 411 | Change the way the backend is configured, simplifying it. 412 | 413 | - remove wrapper scripts (hie-vscode.sh/hie-vscode.bat) 414 | - dropdown choice between `haskell-ide-engine`, `haskell-language-server` or 415 | `ghcide` in the `hieVariant` setting. 416 | - this can be overridden by an explicit `hieExecutablePath`, as before. 417 | 418 | ## 0.0.39 419 | 420 | Remove verbose logging option, it is not longer supported. 421 | 422 | ## 0.0.38 423 | 424 | Bump dependencies 425 | 426 | ## 0.0.37 427 | 428 | Trying again, working 0.0.35 429 | 430 | - Add Restart command (@gdziadkiewicz) 431 | - Add Ormolu as a formatter option (@DavSanchez) 432 | - Update README 433 | 434 | ## 0.0.36 435 | 436 | - Roll back to 0.0.34 437 | 438 | ## 0.0.35 439 | 440 | - Add Restart command (@gdziadkiewicz) 441 | - Add Ormolu as a formatter option (@DavSanchez) 442 | - Update README 443 | 444 | ## 0.0.34 445 | 446 | - Remove --lsp parameter from hie-vscode.bat 447 | 448 | ## 0.0.33 449 | 450 | - Introduced configuration setting `noLspParam`, default `false` to control 451 | setting the `--lsp` flag for the hie server. So by default we will set the 452 | command line argument for the server, but it can be turned off. 453 | 454 | ## 0.0.32 455 | 456 | - Re-enable the `--lsp` flag for the hie server 457 | - Update some deps for security vulnerabilities 458 | 459 | ## 0.0.31 460 | 461 | - Log to stderr (vscode output) by default, add option for logfile (@bubba) 462 | 463 | ## 0.0.30 464 | 465 | - Bundle using webpack (@chrismwendt) 466 | - Bump protocol version to 3.15 prerelease (@alanz) 467 | This allows working progress reporting from hie. 468 | - Update casesplit plugin (@Avi-D-coder) 469 | 470 | ## 0.0.29 471 | 472 | - bump protocol version to 3.15 (prerelease) (@alanz) 473 | - upgrade deps, including avoiding vulnerabilities on lodash (@alanz) 474 | - warn about compile time and wrapped hie (@janat08) 475 | 476 | ## 0.0.28 477 | 478 | - remove unused `lsp` flag (@bubba) 479 | - do not start `hie` if `hie-wrapper` crashes (@bubba) 480 | - Expose diagnosticsOnChange option for settings (Frederik Ramcke) 481 | - Avoid CVE on `extend` package 482 | - Enable displaying window progress (@bubba) 483 | 484 | ## 0.0.27 485 | 486 | - Re-enable search feature for documentation (@anonimitoraf) 487 | Accesed via `ctrl-f`. 488 | 489 | ## 0.0.26 490 | 491 | - Show documentation content using Webview API (@EdAllonby) 492 | - npm audit fix (@alanz) 493 | 494 | ## 0.0.25 495 | 496 | - Add vsce dependency to "Contributing" document (@EdAllonby) 497 | - Add formatterProvider config (@bubba) 498 | - Bugfix for stack version on windows (@beauzeaux) 499 | - Update settings to match hie version 0.7.0.0 (@alanz) 500 | - npm audit fix (@bubba) 501 | 502 | ## 0.0.24 503 | 504 | - Add snippet config option (@bubba) 505 | 506 | ## 0.0.23 507 | 508 | - Fix multi-process issue, where vscode would launch multiple hie instances. 509 | By @kfigiela 510 | 511 | ## 0.0.22 512 | 513 | - Add configuration option to enable liquid haskell processing. This 514 | is a preview feature of hie from 515 | ca2d3eaa19da8ec9d55521b461d8e2e8cffee697 on 2019-09-05. 516 | 517 | ## 0.0.21 518 | 519 | - Remove languageServerHaskell.useHieWrapper, We now use hie-wrapper 520 | by default. 521 | - Update the vscode-languageclient to v4.4.0 522 | - Fix #98 Import identifier insertion line `moduleLine` is now the 523 | first line that is (trimmed) `where` or ends with `where` or ends 524 | with `)where`. (@mpilgrem) 525 | 526 | ## 0.0.20 527 | 528 | - Add the case-split function (@txsmith). Required hie >= 0.2.1.0 529 | - Update the vscode-languageclient to v4.2.0 (@Bubba) 530 | - Use the hie-wrapper executable now installed with hie to choose the 531 | right version of hie to use for the given project. 532 | 533 | ## 0.0.19 534 | 535 | - Fix hie launch on windows with logging off (#90). Thanks @Tehnix. 536 | 537 | ## 0.0.18 538 | 539 | - Support GHC 8.4.3 in the wrapper file 540 | - The `languageServerHaskell.trace.server` parameter now affects 541 | `/tmp/hie.log`, as well as ghc-mod `--vomit` output. 542 | - Add an Import identifier command, by @chrismwendt 543 | 544 | ## 0.0.17 545 | 546 | - Support GHC 8.4.2 in the wrapper file 547 | - Update dependencies to avoid security vulnerability. 548 | - Use os.tmpdir() for the hie.log file 549 | 550 | ## 0.0.15 551 | 552 | Support the new webview-api for the documentation browser, thanks to @AlexeyRaga. 553 | 554 | ## 0.0.14 555 | 556 | Revert `vscode-languageclient` dependency to version 3.5.0, since version 4.x for some 557 | reason breaks the documentation browser. 558 | 559 | ## 0.0.13 560 | 561 | Add configuration to set the path to your HIE executable, if it's not on your PATH. Note 562 | that this adds the `--lsp` argument to the call of this executable. 563 | 564 | ## 0.0.12 565 | 566 | Add configuration to enable/disable HIE, useful for multi-root workspaces. 567 | 568 | ## 0.0.11 569 | 570 | Add additional marketplace categories. 571 | 572 | ## 0.0.10 573 | 574 | Add support for multi-root workspaces, thanks to @tehnix. See the README section 575 | on [_Using multi-root workspaces_](https://github.com/alanz/vscode-hie-server#using-multi-root-workspaces) for more. 576 | 577 | ## 0.0.9 578 | 579 | Publish to the visual studio marketplace through travis CI via git tags. E.g. 580 | `git tag -a 0.0.9 -m "Version 0.0.9"` and then `git push origin 0.0.9`. 581 | 582 | ## 0.0.8 583 | 584 | Add new haskell-ide-engine logo, thanks to @damienflament 585 | 586 | Add rudimentary support for detecting the project GHC version and using the 587 | appropriate hie version. This currently only works on Linux (contributors on 588 | other platforms, please jump in with appropriate scripts) and requires 589 | `haskell-ide-engine` built via the `Makefile` added in 590 | https://github.com/haskell/haskell-ide-engine/pull/447. Thanks to @Tehnix 591 | 592 | ## 0.0.7 593 | 594 | Update `package-lock.json` to fresh dependencies. 595 | 596 | Add show type _of selected expression_ on hover feature, by @halhenke 597 | 598 | Added options for how to display the same information when using the show type 599 | command menu, by @halhenke 600 | 601 | Moved the configuration setting about showing trace information into the proper 602 | scope, by @halhenke 603 | 604 | ## 0.0.6 605 | 606 | Update `package-lock.json` to fresh dependencies. 607 | 608 | Update the installation check on Win32 platforms, by @soylens. 609 | 610 | Use `tslint` on the plugin sources, by @halhenke. 611 | 612 | ## 0.0.5 613 | 614 | Stop the output channel from taking focus on startup, by @Tehnix and @halhenke 615 | 616 | Rework and improve the document layout, for gihub and the marketplace, by @Tehnix 617 | 618 | Set up Travis testing an potential auto-deply to marketplace, by @Tehnix 619 | 620 | ## 0.0.4 621 | 622 | Show documents in a tab, by @AlexeyRaga 623 | 624 | Add a configuration option to enable/disable `hlint`. 625 | 626 | ## 0.0.3 627 | 628 | Add "Haskell: Show type" command, bound to Ctrl-alt-t (Cmd-alt-t on mac). This 629 | calls the `ghc-mod` `type` command on the current cursor location or highlighted 630 | region. Thanks to @AlexeyRaga 631 | 632 | Add a check for having the `hie` executable in the path on startup, to prevent 633 | an endless failure to start if the executable is not there. Thanks to @DavidEichman 634 | 635 | ## 0.0.2 636 | 637 | Add some HaRe commands, accesible via the command palette. 638 | 639 | ## 0.0.1 640 | 641 | Initial release of haskell-ide-engine VS Code extension, for brave pioneers. 642 | -------------------------------------------------------------------------------- /GenChangelogs.hs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env cabal 2 | {- cabal: 3 | build-depends: base, bytestring, process, text, github, time >= 1.9 4 | -} 5 | 6 | {-# LANGUAGE OverloadedStrings #-} 7 | {-# LANGUAGE RecordWildCards #-} 8 | 9 | import Control.Monad 10 | import qualified Data.ByteString.Char8 as BS 11 | import Data.List 12 | import Data.Maybe 13 | import qualified Data.Text as T 14 | import Data.Time.Format.ISO8601 15 | import Data.Time.LocalTime 16 | import GitHub 17 | import System.Environment 18 | import System.Process 19 | 20 | main = do 21 | callCommand "git fetch --tags" 22 | tag <- last . lines <$> 23 | readProcess "git" ["tag", "--list", "--sort=v:refname"] "" 24 | 25 | lastDateStr <- last . lines <$> readProcess "git" ["show", "-s", "--format=%cI", "-1", tag] "" 26 | lastDate <- zonedTimeToUTC <$> iso8601ParseM lastDateStr 27 | 28 | args <- getArgs 29 | let githubReq = case args of 30 | [] -> github' 31 | token:_ -> github (OAuth $ BS.pack token) 32 | prs <- githubReq $ pullRequestsForR "haskell" "vscode-haskell" stateClosed FetchAll 33 | let prsAfterLastTag = either (error . show) 34 | (foldMap (\pr -> [pr | inRange pr, isNotDependabot pr])) 35 | prs 36 | inRange pr 37 | | Just mergedDate <- simplePullRequestMergedAt pr = mergedDate > lastDate 38 | | otherwise = False 39 | 40 | isNotDependabot SimplePullRequest{..} = 41 | untagName (simpleUserLogin simplePullRequestUser) /= "dependabot[bot]" 42 | 43 | forM_ prsAfterLastTag $ \SimplePullRequest{..} -> 44 | putStrLn $ T.unpack $ "- " <> simplePullRequestTitle <> "\n" <> 45 | " ([#" <> T.pack (show $ unIssueNumber simplePullRequestNumber) <> "](" <> getUrl simplePullRequestHtmlUrl <> "))" <> 46 | " by @" <> untagName (simpleUserLogin simplePullRequestUser) 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Based on https://github.com/Microsoft/vscode-languageserver-node-example 2 | which has the following license requirement : 3 | 4 | ----------------------------------------------------- 5 | 6 | Copyright (c) Microsoft Corporation 7 | 8 | All rights reserved. 9 | 10 | MIT License 11 | 12 | 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: 13 | 14 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 15 | 16 | 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Haskell for Visual Studio Code 2 | 3 | [![vsmarketplacebadge](https://vsmarketplacebadges.dev/version/haskell.haskell.png)](https://marketplace.visualstudio.com/items?itemName=haskell.haskell) 4 | 5 | This extension adds language support for [Haskell](https://haskell.org), powered by the [Haskell Language Server](https://github.com/haskell/haskell-language-server). 6 | As almost all features are provided by the server you might find interesting read its [documentation](https://haskell-language-server.readthedocs.io). 7 | 8 | ## Table of Contents 9 | 10 | - [Haskell for Visual Studio Code](#haskell-for-visual-studio-code) 11 | - [Table of Contents](#table-of-contents) 12 | - [Setup](#setup) 13 | - [Features](#features) 14 | - [Requirements](#requirements) 15 | - [Configuration options](#configuration-options) 16 | - [Path to server executable](#path-to-server-executable) 17 | - [Security warning](#security-warning) 18 | - [Set additional environment variables for the server](#set-additional-environment-variables-for-the-server) 19 | - [Downloaded binaries](#downloaded-binaries) 20 | - [Setting a specific toolchain](#setting-a-specific-toolchain) 21 | - [Supported GHC versions](#supported-ghc-versions) 22 | - [Using multi-root workspaces](#using-multi-root-workspaces) 23 | - [Investigating and reporting problems](#investigating-and-reporting-problems) 24 | - [FAQ](#faq) 25 | - [Troubleshooting](#troubleshooting) 26 | - [Check issues and tips in the haskell-language-server project](#check-issues-and-tips-in-the-haskell-language-server-project) 27 | - [Restarting the language server](#restarting-the-language-server) 28 | - [`Failed to get project GHC version` on darwin M1 with stack](#failed-to-get-project-ghc-version-on-darwin-m1-with-stack) 29 | - [`GHC ABIs don't match`](#ghc-abis-dont-match) 30 | - [Using an old configuration](#using-an-old-configuration) 31 | - [Stack/Cabal/GHC can not be found](#stackcabalghc-can-not-be-found) 32 | - [Contributing](#contributing) 33 | - [Release Notes](#release-notes) 34 | 35 | ## Setup 36 | 37 | This Extension comes with "batteries"-included and can manage your Haskell Language Server installations for you, 38 | powered by [GHCup](https://www.haskell.org/ghcup/). 39 | Installation of [GHCup](https://www.haskell.org/ghcup/) can not happen automatically, so if you want your HLS installations to be 40 | managed by the Extension, you will have to follow the [installation instructions for GHCup](https://www.haskell.org/ghcup/). 41 | 42 | **Note:** Make sure you have a working `ghcup` installation, before launching the Extension. 43 | 44 | ## Features 45 | 46 | You can watch demos for some of these features [here](https://haskell-language-server.readthedocs.io/en/latest/features.html#demos). 47 | 48 | - Warning and error diagnostics from GHC 49 | - Type information and documentation on hover 50 | - Jump to definition: [for now only for local code definitions](https://github.com/haskell/haskell-language-server/issues/708) 51 | - Document symbols 52 | - Highlight references in document 53 | - Code completion 54 | - Show documentation and sources in hackage 55 | - Formatting via [Brittany](https://github.com/lspitzner/brittany), [Floskell](https://github.com/ennocramer/floskell), [Fourmolu](https://github.com/fourmolu/fourmolu), [Ormolu](https://github.com/tweag/ormolu) or [Stylish Haskell](https://github.com/haskell/stylish-haskell) 56 | - [Multi-root workspace](https://code.visualstudio.com/docs/editor/multi-root-workspaces) support 57 | - [Code evaluation](https://haskell-language-server.readthedocs.io/en/latest/features.html#code-evaluation), see its [Tutorial](https://github.com/haskell/haskell-language-server/blob/master/plugins/hls-eval-plugin/README.md) 58 | - [Integration with](https://haskell-language-server.readthedocs.io/en/latest/features.html#retrie-integration) [retrie](https://hackage.haskell.org/package/retrie), a powerful, easy-to-use codemodding tool 59 | - [Code lenses for explicit import lists](https://haskell-language-server.readthedocs.io/en/latest/features.html#explicit-import-lists) 60 | - [Generate functions from type signatures, and intelligently complete holes using](https://haskell-language-server.readthedocs.io/en/latest/features.html#wingman) [Wingman (tactics)](https://github.com/haskell/haskell-language-server/tree/master/plugins/hls-tactics-plugin) 61 | - [Integration](https://haskell-language-server.readthedocs.io/en/latest/features.html#hlint) with [hlint](https://github.com/ndmitchell/hlint), the most used haskell linter, to show diagnostics and apply hints via [apply-refact](https://github.com/mpickering/apply-refact) 62 | - [Module name suggestions](https://haskell-language-server.readthedocs.io/en/latest/features.html#module-names) for insertion or correction 63 | - [Call hierarchy support](https://haskell-language-server.readthedocs.io/en/latest/features.html#call-hierarchy) 64 | 65 | ## Requirements 66 | 67 | - For standalone `.hs`/`.lhs` files, [ghc](https://www.haskell.org/ghc/) must be installed and on the PATH. The easiest way to install it is with [ghcup](https://www.haskell.org/ghcup/). 68 | - For Cabal based projects, both ghc and [cabal-install](https://www.haskell.org/cabal/) must be installed and on the PATH. It can also be installed with [ghcup](https://www.haskell.org/ghcup/) or [Chocolatey](https://www.haskell.org/platform/windows.html) on Windows. 69 | - For Stack based projects, [stack](http://haskellstack.org) must be installed and on the PATH and must be [configured to use GHC binaries installed by GHCup](https://www.haskell.org/ghcup/guide/#stack-integration). 70 | - If you are installing from an offline VSIX file, you need to install [language-haskell](https://github.com/JustusAdam/language-haskell) too after installation (either from the marketplace or offline). 71 | - Alternatively, you can let the extension manage your entire toolchain automatically (you'll be asked on first startup) via 72 | [ghcup](https://www.haskell.org/ghcup/), which should be pre-installed 73 | 74 | ## Configuration options 75 | 76 | For a general picture about the server configuration, including the project setup, [you can consult the server documentation about the topic](https://haskell-language-server.readthedocs.io/en/latest/configuration.html). 77 | 78 | For information on how to set configuration in VSCode, see [here](https://code.visualstudio.com/docs/getstarted/settings). 79 | 80 | ### Path to server executable 81 | 82 | If your server is manually installed and not on your path, you can also manually set the path to the executable. 83 | 84 | ```json 85 | "haskell.serverExecutablePath": "~/.local/bin/haskell-language-server" 86 | ``` 87 | 88 | There are a few placeholders which will be expanded: 89 | 90 | - `~`, `${HOME}` and `${home}` will be expanded into your users' home folder. 91 | - `${workspaceFolder}` and `${workspaceRoot}` will expand into your current project root. 92 | 93 | #### Security warning 94 | 95 | The option has `machine-overridable` scope so it can be changed per workspace. 96 | This supposes it could be used to execute arbitrary programs adding a `.vscode/settings.json` in the workspace folder including this option with the appropriate path. 97 | See [#387](https://github.com/haskell/vscode-haskell/issues/387) for more details. 98 | 99 | ### Set additional environment variables for the server 100 | 101 | You can add additional environment variables for the lsp server using the configuration option `haskell.serverEnvironment`. For example, to change the cache directory used by the server you could set: 102 | 103 | ```json 104 | { "haskell.serverEnvironment": { "XDG_CACHE_HOME": "/path/to/my/cache" } } 105 | ``` 106 | 107 | as the server uses the XDG specification for cache directories. 108 | 109 | The environment _only will be visible for the lsp server_, not for other extension tasks like find the server executable. 110 | 111 | ### Downloaded binaries 112 | 113 | This extension will download `haskell-language-server` binaries and the rest of the toolchain if you selected to use GHCup during 114 | first start. Check the `haskell.manageHLS` setting. 115 | 116 | It will then download the newest version of haskell-language-server which has support for the required ghc. 117 | That means it could use an older version than the latest one, without the last features and bug fixes. 118 | For example, if a project needs ghc-8.10.4 the extension will download and use haskell-language-server-1.4.0, the latest version which supported ghc-8.10.4. Even if the latest global haskell language-server version is 1.5.1. 119 | 120 | If you have disk space issues, check `ghcup gc --help`. 121 | 122 | You can also instruct the extension to use a different installation directory for the toolchain, 123 | e.g. to not interfere with system GHCup installation. Depending on your platform, add the full 124 | resolved path like so: 125 | 126 | ```json 127 | "haskell.serverEnvironment": { 128 | "GHCUP_INSTALL_BASE_PREFIX": "/home/foo/.config/Code/User/globalStorage/haskell.haskell/" 129 | } 130 | ``` 131 | 132 | The internal storage paths for the extension depend on the platform: 133 | 134 | | Platform | Path | 135 | | -------- | ------------------------------------------------------------------------------- | 136 | | macOS | `~/Library/Application\ Support/Code/User/globalStorage/haskell.haskell/.ghcup` | 137 | | Windows | `%APPDATA%\Code\User\globalStorage\haskell.haskell\ghcup` | 138 | | Linux | `$HOME/.config/Code/User/globalStorage/haskell.haskell/.ghcup` | 139 | 140 | If you want to manage HLS yourself, set `haskell.manageHLS` to `PATH` and make sure HLS is in your PATH 141 | or set `haskell.serverExecutablePath` (overrides all other settings) to a valid executable. 142 | 143 | If you need to set mirrors for ghcup download info, check the settings `haskell.metadataURL` and `haskell.releasesURL`. 144 | 145 | ### Setting a specific toolchain 146 | 147 | When `manageHLS` is set to `GHCup`, you can define a specific toolchain (`hls`, `ghc`, `cabal` and `stack`), 148 | either globally or per project. E.g.: 149 | 150 | ```json 151 | { 152 | "haskell.toolchain": { 153 | "hls": "1.6.1.1", 154 | "cabal": "recommended", 155 | "stack": null 156 | } 157 | } 158 | ``` 159 | 160 | This means: 161 | 162 | 1. install the `ghc` version corresponding to the project (default, because it's omitted) 163 | 2. install `hls` 1.6.1.1 164 | 3. install the recommended `cabal` version from ghcup 165 | 4. don't install any `stack` version 166 | 167 | Another config could be: 168 | 169 | ```json 170 | { 171 | "haskell.toolchain": { 172 | "ghc": "9.2.2", 173 | "hls": "latest", 174 | "cabal": "recommended" 175 | } 176 | } 177 | ``` 178 | 179 | Meaning: 180 | 181 | 1. install `ghc` 9.2.2 regardless of what the project requires 182 | 2. always install latest `hls`, even if it doesn't support the given GHC version 183 | 3. install recommended `cabal` 184 | 4. install latest `stack` (default, because it's omitted) 185 | 186 | The defaults (when omitted) are as follows: 187 | 188 | 1. install the project required `ghc` (corresponding to `with-compiler` setting in `cabal.project` for example) 189 | 2. install the latest `hls` version that supports the project required ghc version 190 | 3. install latest `cabal` 191 | 4. install latest `stack` 192 | 193 | When a the value is `null`, the extension will refrain from installing it. 194 | 195 | At last, if you don't want `ghcup` to manage any of the external tools except `hls`, you can use: 196 | 197 | ```json 198 | { 199 | "haskell.toolchain": { 200 | "ghc": null, 201 | "cabal": null, 202 | "stack": null 203 | } 204 | } 205 | ``` 206 | 207 | ### Supported GHC versions 208 | 209 | You can check each GHC version's support status and the policy followed for deprecations [here](https://haskell-language-server.readthedocs.io/en/latest/support/ghc-version-support.html). 210 | 211 | [Building from source](https://haskell-language-server.readthedocs.io/en/latest/installation.html) may support more versions! 212 | 213 | The exact list of binaries can be checked in the last release of haskell-language-server: 214 | 215 | ## Using multi-root workspaces 216 | 217 | First, check out [what multi-root workspaces](https://code.visualstudio.com/docs/editor/multi-root-workspaces) are. The idea of using multi-root workspaces, is to be able to work on several different Haskell projects, where the GHC version or stackage LTS could differ, and have it work smoothly. 218 | 219 | The language server is now started for each workspace folder you have in your multi-root workspace, and several configurations are on a resource (i.e. folder) scope, instead of window (i.e. global) scope. 220 | 221 | ## Investigating and reporting problems 222 | 223 | 1. Go to extensions and right click `Haskell` and choose `Extensions Settings` 224 | 2. Scroll down to `Haskell › Trace: Server` and set it to `messages`. 225 | 3. Set `Haskell › Trace: Client` to `debug`. It will print all the environment variables so take care it does not contain any sensitive information before sharing it. 226 | 4. Restart vscode and reproduce your problem 227 | 5. Go to the main menu and choose `View -> Output` (`Ctrl + Shift + U`) 228 | 6. On the new Output panel that opens on the right side in the drop down menu choose `Haskell ()` 229 | 230 | Please include the output when filing any issues on the [haskell-language-server](https://github.com/haskell/haskell-language-server/issues/new) issue tracker. 231 | 232 | ## FAQ 233 | 234 | ### Troubleshooting 235 | 236 | #### Check issues and tips in the haskell-language-server project 237 | 238 | - Usually the error or unexpected behaviour is already reported in the [haskell language server issue tracker](https://github.com/haskell/haskell-language-server/issues). Finding the issue could be useful to help resolve it and sometimes includes a workaround for the issue. 239 | - You can also check the [troubleshooting section](https://haskell-language-server.readthedocs.io/en/latest/troubleshooting.html) in the server documentation. 240 | 241 | #### Restarting the language server 242 | 243 | - Sometimes the language server might get stuck in a rut and stop responding to your latest changes. 244 | Should this occur you can try restarting the language server with Ctrl shift P/ shift P > Restart Haskell LSP Server. 245 | 246 | #### `Failed to get project GHC version` on darwin M1 with stack 247 | 248 | If you have installed stack via the official cannels, the binary will not be M1 native, but x86 and trigger the rosetta compatibility layer. GHCup provides real stack/HLS M1 binaries, so make sure you install stack via GHCup. Also see https://github.com/haskell/haskell-language-server/issues/2864 249 | 250 | #### `GHC ABIs don't match` 251 | 252 | If you are using certain versions of GHC (such as 9.0.2 or 9.2.4) while running Stack, you may encounter this issue due to an outdated GHC bindist installed by Stack. The vscode-haskell extension does not support the use of GHC binaries managed by Stack. Therefore, it is recommended to configure Stack to allow GHCup to install these binaries instead. Please [refer to the instructions provided by ghcup for this purpose](https://www.haskell.org/ghcup/guide/#stack-integration). 253 | 254 | If you really want to use GHC 9.0.2 managed by Stack, force it to install the fixed bindist (that includes profiling libs) by adding this to your stack.yaml (depending on your platform): 255 | 256 | ```yml 257 | setup-info: 258 | ghc: 259 | linux64-tinfo6: 260 | 9.0.2: 261 | url: 'https://downloads.haskell.org/ghc/9.0.2/ghc-9.0.2a-x86_64-fedora27-linux.tar.xz' 262 | ``` 263 | 264 | Now make sure to remove cached/installed libraries to avoid getting segfaults at runtime. 265 | 266 | As a final workaround, you can try to compile HLS from source (the extension should pick it up) via ghcup, see [https://haskell-language-server.readthedocs.io/en/stable/installation.html#ghcup](https://haskell-language-server.readthedocs.io/en/stable/installation.html#ghcup). In any case, the recommended approach is to let GHCup install the GHC binaries. 267 | 268 | #### `hGetContents: invalid argument (invalid byte sequence)` 269 | 270 | This problem was encountered on darwin M2 with ghcup. 271 | Should you see the error that the "Haskell server crashed 5 times in the last 3 minutes," you can check the Haskell output to see whether this was due to an error mentioning `hGetContents: invalid argument (invalid byte sequence)`. 272 | If this is the case, setting `terminal.integrated.detectLocale` to `off` might resolve your issue. 273 | 274 | #### Using an old configuration 275 | 276 | If something just doesn't work, but you recall an old configuration that did, you 277 | may try forcing a particular setting, e.g. by disabling all automatic installations 278 | except HLS: 279 | 280 | ```json 281 | "haskell.toolchain": { 282 | "hls": "1.6.1.0", 283 | "ghc": null, 284 | "cabal": null, 285 | "stack": null 286 | } 287 | ``` 288 | 289 | #### Stack/Cabal/GHC can not be found 290 | 291 | Also make sure GHCup is installed and in `$PATH`. If you're not starting VSCode from the terminal, you might need to add `${HOME}/.ghcup/bin` to PATH like so: 292 | 293 | ```json 294 | "haskell.serverEnvironment": { 295 | "PATH": "${HOME}/.ghcup/bin:$PATH" 296 | } 297 | ``` 298 | 299 | ## Contributing 300 | 301 | If you want to help, get started by reading [Contributing](./docs/Contributing.md) for more details. 302 | 303 | ## Release Notes 304 | 305 | See the [Changelog](./Changelog.md) for more details. 306 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {} }: 2 | with pkgs; 3 | 4 | # Please run `yarn import` first to generate yarn.lock! 5 | pkgs.mkYarnPackage rec { 6 | name = "haskell"; 7 | src = ./.; 8 | packageJSON = ./package.json; 9 | yarnLock = ./yarn.lock; 10 | 11 | installPhase = '' 12 | mkdir -p "$out/dist" 13 | yarn vscode:prepublish --output-path "$out/dist" 14 | mv deps/${name}/package.json "$out" 15 | ''; 16 | 17 | distPhase = '' 18 | true 19 | ''; 20 | } 21 | -------------------------------------------------------------------------------- /docs/Contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Dependencies and Building 4 | 5 | Run `yarn install` in the project root to install the development dependencies. 6 | 7 | You can also package up the extension with 8 | 9 | - `yarn global add vsce` to get the Extension Manager, 10 | - `yarn install` to build the extension 11 | - `vsce package` which creates an extension package at `haskell-.vsix`. 12 | 13 | _Note:_ that if you get errors running `vsce package`, it might help running `yarn run pretest` directly, since that gives the actual error output of the TypeScript compilation. 14 | 15 | ## Developing inside VS Code 16 | 17 | - Launch VS Code, press `File` > `Open Folder`, open the `vscode-haskell` folder; 18 | - press `F5` to open a new window with the `vscode-haskell` loaded (this will overwrite existing ones, e.g. from the marketplace); 19 | - open a Haskell file with the **new** editor to test the LSP client; 20 | 21 | You are now ready to make changes and debug. You can, 22 | 23 | - set breakpoints in your code inside `src/extension.ts` to debug your extension; 24 | - find output from your extension in the debug console; 25 | - make changes to the code, and then 26 | - relaunch the extension from the debug toolbar 27 | 28 | _Note_: you can also reload (`Ctrl+R` or `Cmd+R` on macOS) the VS Code window with your extension to load your changes 29 | 30 | #### Formatting 31 | 32 | [prettier](https://prettier.io) is automatically run on each commit via husky. If you are developing within VS Code, the settings are set to auto format on save. 33 | The configurations for prettier are located in `.prettierrc`. 34 | 35 | ## Navigating the Files 36 | 37 | A brief overview of the files, 38 | 39 | - `package.json` contains the basic information about the package, see [the full manifest for more](https://code.visualstudio.com/docs/extensionAPI/extension-manifest), such as telling VS Code which scope the LSP works on (Haskell and Literate Haskell in our case), and possible configuration 40 | - `src/extension.ts` is the main entrypoint to the extension, and handles launching the language server. 41 | - `src/hlsBinaries.ts` handles automatically installing the pre-built `haskell-language-server` binaries 42 | - `src/utils.ts` has some functions for downloading files and checking if executables are on the path 43 | - `src/docsBrowser.ts` contains the logic for displaying the documentation browser (e.g. hover over a type like `mapM_` and click `Documentation` or `Source`) 44 | 45 | ## Helpful Reading Material 46 | 47 | We recommend checking out [Your First VS Code Extension](https://code.visualstudio.com/docs/extensions/example-hello-world) and [Creating a Language Server](https://code.visualstudio.com/docs/extensions/example-language-server) for some introduction to VS Code extensions. 48 | -------------------------------------------------------------------------------- /docs/Release.md: -------------------------------------------------------------------------------- 1 | # Release Checklist 2 | 3 | Follow this list for items that must be completed for release of the `vscode-haskell` extension. 4 | 5 | - [ ] Run `yarn audit` for security vulnerabilities. 6 | - Fix vulnerabilities. 7 | - [ ] Run `yarn outdated` to find outdated package version, review what needs to be updated. 8 | - `yarn upgrade-interactive` and `yarn upgrade-interactive --latest` is helpful here. 9 | - [ ] Run `cat test/testdata/schema/*/vscode-extension-schema.golden.json | jq --sort-keys -s add` in the `haskell-language-server` repo and add new configuration items. 10 | - [ ] SemVer Compatible Version Bump in `package.json` 11 | - For pre-releases, we follow the version convention at: https://code.visualstudio.com/api/working-with-extensions/publishing-extension#prerelease-extensions. We use `major.EVEN_NUMBER.patch` for release versions and `major.ODD_NUMBER.patch` for pre-release versions. For example: `2.0.*` for release and `2.1.*` for pre-release. 12 | - [ ] Update ChangeLog.md. The output of `./GenChangelogs.hs` usually suffices. 13 | - [ ] Update the README.md to have no outdated information. 14 | - [ ] Make sure CI is succeeding. 15 | - [ ] Perform the release by creating a [release in Github](https://github.com/haskell/vscode-haskell/releases) 16 | - Github actions will automatically release the extension to VSCode- and VSX-Marketplace. 17 | - If you want to create a pre-release, create a [pre-release in Github](https://github.com/haskell/vscode-haskell/releases). The github action will perform the appropriate actions automatically and publish the pre-release of the extension to VSCode- and VSX-Marketplace. 18 | 19 | ## Branching policy 20 | 21 | Sometimes there is a release (stable) and pre-release (unstable) at the same time and we need to do a release for the stable release and sometimes we need to do a release for the pre-release series. 22 | To simplify the release management, the following policy is in place: 23 | 24 | - The branch `master` contains the current pre-release 25 | - As such, its `package.json` must always have the form `major.ODD_NUMBER.patch` 26 | - Dependency version bumps are automatically performed by dependabot against `master` 27 | - For each release, a tag must be created 28 | - Stable releases are located on a separate branch called `release-` 29 | - Before a release, the branch is rebased on top of current master 30 | - For each stable release, a tag must be created of the form `major.EVEN_NUMBER.patch` 31 | 32 | ## Release CI 33 | 34 | The release CI has access tokens for VSX Marketplace and the VSCode Marketplace. 35 | 36 | Seemingly, the VSX Marketplace token does not expire. If it is lost for some reason, follow the steps below. Fendor can also generate a VSX Marketplace token. 37 | 38 | The latter needs to be refreshed once a year. 39 | 40 | - Send an email to `committee@haskell.org` requesting the token 41 | - Include your public GPG key so they can send you the token encrypted 42 | - Update the repository secrets 43 | - People from the [@haskell-ide](https://github.com/orgs/haskell/teams/haskell-ide) have full access to the vscode-haskell repo and can update secrets 44 | 45 | Last time the VSCode Marketplace token was updated: 2023-08-17 46 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from 'globals'; 2 | import eslint from '@eslint/js'; 3 | import tseslint from 'typescript-eslint'; 4 | 5 | export default tseslint.config( 6 | { files: ['**/*.{js,mjs,cjs,ts}'] }, 7 | { 8 | languageOptions: { 9 | globals: globals.node, 10 | parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname }, 11 | }, 12 | }, 13 | { 14 | // disables type checking for this file only 15 | files: ['eslint.config.mjs'], 16 | extends: [tseslint.configs.disableTypeChecked], 17 | }, 18 | eslint.configs.recommended, 19 | tseslint.configs.recommendedTypeChecked, 20 | { 21 | rules: { 22 | // turn off these lints as we access workspaceConfiguration fields. 23 | // So far, there was no bug found with these unsafe accesses. 24 | '@typescript-eslint/no-unsafe-assignment': 'off', 25 | '@typescript-eslint/no-unsafe-member-access': 'off', 26 | // Sometimes, the 'any' just saves too much time. 27 | '@typescript-eslint/no-explicit-any': 'off', 28 | '@typescript-eslint/no-floating-promises': 'error', 29 | '@typescript-eslint/no-unused-vars': [ 30 | 'error', 31 | { 32 | args: 'all', 33 | argsIgnorePattern: '^_', 34 | caughtErrors: 'all', 35 | caughtErrorsIgnorePattern: '^_', 36 | destructuredArrayIgnorePattern: '^_', 37 | varsIgnorePattern: '^_', 38 | ignoreRestSiblings: true, 39 | }, 40 | ], 41 | }, 42 | }, 43 | ); 44 | -------------------------------------------------------------------------------- /images/hls-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haskell/vscode-haskell/HEAD/images/hls-logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "haskell", 3 | "displayName": "Haskell", 4 | "description": "Haskell language support powered by the Haskell Language Server", 5 | "version": "2.7.0", 6 | "license": "MIT", 7 | "publisher": "haskell", 8 | "engines": { 9 | "vscode": "^1.102.0" 10 | }, 11 | "keywords": [ 12 | "language", 13 | "haskell", 14 | "cabal", 15 | "stack", 16 | "lsp", 17 | "multi-root ready" 18 | ], 19 | "homepage": "https://github.com/haskell/vscode-haskell", 20 | "repository": "https://github.com/haskell/vscode-haskell.git", 21 | "bugs": { 22 | "url": "https://github.com/haskell/vscode-haskell/issues" 23 | }, 24 | "categories": [ 25 | "Programming Languages", 26 | "Formatters", 27 | "Linters", 28 | "Other" 29 | ], 30 | "icon": "images/hls-logo.png", 31 | "galleryBanner": { 32 | "color": "#22172A", 33 | "theme": "dark" 34 | }, 35 | "activationEvents": [ 36 | "onLanguage:haskell", 37 | "onLanguage:literate haskell", 38 | "onLanguage:cabal" 39 | ], 40 | "main": "./dist/extension", 41 | "contributes": { 42 | "languages": [ 43 | { 44 | "id": "haskell", 45 | "aliases": [ 46 | "Haskell", 47 | "haskell" 48 | ], 49 | "extensions": [ 50 | ".hs" 51 | ] 52 | }, 53 | { 54 | "id": "cabal", 55 | "aliases": [ 56 | "Cabal" 57 | ], 58 | "extensions": [ 59 | ".cabal" 60 | ] 61 | }, 62 | { 63 | "id": "literate haskell", 64 | "aliases": [ 65 | "Literate Haskell", 66 | "literate Haskell" 67 | ], 68 | "extensions": [ 69 | ".lhs" 70 | ] 71 | } 72 | ], 73 | "configuration": { 74 | "type": "object", 75 | "title": "Haskell", 76 | "properties": { 77 | "haskell.formattingProvider": { 78 | "scope": "resource", 79 | "type": "string", 80 | "enum": [ 81 | "brittany", 82 | "floskell", 83 | "fourmolu", 84 | "ormolu", 85 | "stylish-haskell", 86 | "none" 87 | ], 88 | "default": "ormolu", 89 | "description": "The formatter to use when formatting a document or range. Ensure the plugin is enabled." 90 | }, 91 | "haskell.cabalFormattingProvider": { 92 | "scope": "resource", 93 | "type": "string", 94 | "enum": [ 95 | "cabal-gild", 96 | "cabal-fmt", 97 | "none" 98 | ], 99 | "default": "cabal-gild", 100 | "description": "The formatter to use when formatting a document or range of a cabal formatter. Ensure the plugin is enabled." 101 | }, 102 | "haskell.openDocumentationInHackage": { 103 | "scope": "resource", 104 | "type": "boolean", 105 | "default": true, 106 | "description": "When opening 'Documentation' for external libraries, open in hackage by default. Set to false to instead open in vscode." 107 | }, 108 | "haskell.openSourceInHackage": { 109 | "scope": "resource", 110 | "type": "boolean", 111 | "default": true, 112 | "description": "When opening 'Source' for external libraries, open in hackage by default. Set to false to instead open in vscode." 113 | }, 114 | "haskell.trace.server": { 115 | "scope": "resource", 116 | "type": "string", 117 | "enum": [ 118 | "off", 119 | "messages", 120 | "verbose" 121 | ], 122 | "default": "off", 123 | "description": "Traces the communication between VS Code and the language server." 124 | }, 125 | "haskell.trace.client": { 126 | "scope": "resource", 127 | "type": "string", 128 | "enum": [ 129 | "off", 130 | "error", 131 | "info", 132 | "debug" 133 | ], 134 | "default": "info", 135 | "description": "Sets the log level in the client side." 136 | }, 137 | "haskell.logFile": { 138 | "scope": "resource", 139 | "type": "string", 140 | "default": "", 141 | "description": "If set, redirects the logs to a file." 142 | }, 143 | "haskell.releasesURL": { 144 | "scope": "resource", 145 | "type": "string", 146 | "default": "", 147 | "description": "An optional URL to override where ghcup checks for HLS-GHC compatibility list (usually at: https://raw.githubusercontent.com/haskell/ghcup-metadata/master/hls-metadata-0.0.1.json)" 148 | }, 149 | "haskell.metadataURL": { 150 | "scope": "resource", 151 | "type": "string", 152 | "default": "", 153 | "description": "An optional URL to override where ghcup checks for tool download info (usually at: https://raw.githubusercontent.com/haskell/ghcup-metadata/master/ghcup-0.0.7.yaml)" 154 | }, 155 | "haskell.releasesDownloadStoragePath": { 156 | "scope": "resource", 157 | "type": "string", 158 | "default": "", 159 | "markdownDescription": "An optional path where downloaded metadata will be stored. Check the default value [here](https://github.com/haskell/vscode-haskell#downloaded-binaries)" 160 | }, 161 | "haskell.serverExecutablePath": { 162 | "scope": "machine-overridable", 163 | "type": "string", 164 | "default": "", 165 | "markdownDescription": "Manually set a language server executable. Can be something on the $PATH or the full path to the executable itself. Works with `~,` `${HOME}` and `${workspaceFolder}`." 166 | }, 167 | "haskell.serverExtraArgs": { 168 | "scope": "resource", 169 | "type": "string", 170 | "default": "", 171 | "markdownDescription": "Pass additional arguments to the language server." 172 | }, 173 | "haskell.ghcupExecutablePath": { 174 | "scope": "resource", 175 | "type": "string", 176 | "default": "", 177 | "markdownDescription": "Manually set a ghcup executable path." 178 | }, 179 | "haskell.serverEnvironment": { 180 | "scope": "resource", 181 | "type": "object", 182 | "default": {}, 183 | "markdownDescription": "Define environment variables for the language server." 184 | }, 185 | "haskell.promptBeforeDownloads": { 186 | "scope": "machine", 187 | "type": "boolean", 188 | "default": "true", 189 | "markdownDescription": "Prompt before performing any downloads." 190 | }, 191 | "haskell.manageHLS": { 192 | "scope": "resource", 193 | "type": "string", 194 | "default": "PATH", 195 | "description": "How to manage/find HLS installations.", 196 | "enum": [ 197 | "GHCup", 198 | "PATH" 199 | ], 200 | "enumDescriptions": [ 201 | "Will use ghcup and manage Haskell toolchain in the default location (usually '~/.ghcup')", 202 | "Discovers HLS and other executables in system PATH" 203 | ] 204 | }, 205 | "haskell.toolchain": { 206 | "scope": "resource", 207 | "type": "object", 208 | "default": {}, 209 | "description": "When manageHLS is set to GHCup, this can overwrite the automatic toolchain configuration with a more specific one. When a tool is omitted, the extension will manage the version (for 'ghc' we try to figure out the version the project requires). The format is '{\"tool\": \"version\", ...}'. 'version' accepts all identifiers that 'ghcup' accepts." 210 | }, 211 | "haskell.upgradeGHCup": { 212 | "scope": "resource", 213 | "type": "boolean", 214 | "default": true, 215 | "description": "Whether to upgrade GHCup automatically when 'manageHLS' is set to 'GHCup'." 216 | }, 217 | "haskell.checkProject": { 218 | "scope": "resource", 219 | "type": "boolean", 220 | "default": true, 221 | "description": "Whether to typecheck the entire project on load. It could drive to bad performance in large projects." 222 | }, 223 | "haskell.sessionLoading": { 224 | "scope": "resource", 225 | "type": "string", 226 | "enum": [ 227 | "singleComponent", 228 | "multipleComponents" 229 | ], 230 | "default": "singleComponent", 231 | "description": "Preferred approach for loading package components. Setting this to 'multiple components' (EXPERIMENTAL) allows the build tool (such as `cabal` or `stack`) to [load multiple components at once](https://github.com/haskell/cabal/pull/8726), which is a significant improvement.", 232 | "enumDescriptions": [ 233 | "Always load only a single component at a time. This is the most reliable option if you encountered any issues with the other options.", 234 | "Prefer a multiple component session, if the build tool supports it. At the moment, only `cabal` supports multiple components session loading. If the `cabal` version does not support loading multiple components at once, we gracefully fall back to \"singleComponent\" mode." 235 | ] 236 | }, 237 | "haskell.supportCabalFiles": { 238 | "scope": "resource", 239 | "default": "automatic", 240 | "type": "string", 241 | "enum": [ 242 | "enable", 243 | "disable", 244 | "automatic" 245 | ], 246 | "description": "Enable Language Server support for `.cabal` files. Requires Haskell Language Server version >= 1.9.0.0.", 247 | "enumDescriptions": [ 248 | "Enable Language Server support for `.cabal` files", 249 | "Disable Language Server support for `.cabal` files", 250 | "Enable Language Server support for `.cabal` files if the HLS version supports it." 251 | ] 252 | }, 253 | "haskell.maxCompletions": { 254 | "scope": "resource", 255 | "default": 40, 256 | "type": "integer", 257 | "description": "Maximum number of completions sent to the editor." 258 | }, 259 | "haskell.plugin.alternateNumberFormat.globalOn": { 260 | "default": true, 261 | "description": "Enables alternateNumberFormat plugin", 262 | "scope": "resource", 263 | "type": "boolean" 264 | }, 265 | "haskell.plugin.cabal-fmt.config.path": { 266 | "default": "cabal-fmt", 267 | "markdownDescription": "Set path to 'cabal-fmt' executable", 268 | "scope": "resource", 269 | "type": "string" 270 | }, 271 | "haskell.plugin.cabal-gild.config.path": { 272 | "default": "cabal-gild", 273 | "markdownDescription": "Set path to 'cabal-gild' executable", 274 | "scope": "resource", 275 | "type": "string" 276 | }, 277 | "haskell.plugin.cabal.codeActionsOn": { 278 | "default": true, 279 | "description": "Enables cabal code actions", 280 | "scope": "resource", 281 | "type": "boolean" 282 | }, 283 | "haskell.plugin.cabal.completionOn": { 284 | "default": true, 285 | "description": "Enables cabal completions", 286 | "scope": "resource", 287 | "type": "boolean" 288 | }, 289 | "haskell.plugin.cabal.diagnosticsOn": { 290 | "default": true, 291 | "description": "Enables cabal diagnostics", 292 | "scope": "resource", 293 | "type": "boolean" 294 | }, 295 | "haskell.plugin.cabal.hoverOn": { 296 | "default": true, 297 | "description": "Enables cabal hover", 298 | "scope": "resource", 299 | "type": "boolean" 300 | }, 301 | "haskell.plugin.cabal.symbolsOn": { 302 | "default": true, 303 | "description": "Enables cabal symbols", 304 | "scope": "resource", 305 | "type": "boolean" 306 | }, 307 | "haskell.plugin.cabalHaskellIntegration.globalOn": { 308 | "default": true, 309 | "description": "Enables cabalHaskellIntegration plugin", 310 | "scope": "resource", 311 | "type": "boolean" 312 | }, 313 | "haskell.plugin.callHierarchy.globalOn": { 314 | "default": true, 315 | "description": "Enables callHierarchy plugin", 316 | "scope": "resource", 317 | "type": "boolean" 318 | }, 319 | "haskell.plugin.changeTypeSignature.globalOn": { 320 | "default": true, 321 | "description": "Enables changeTypeSignature plugin", 322 | "scope": "resource", 323 | "type": "boolean" 324 | }, 325 | "haskell.plugin.class.codeActionsOn": { 326 | "default": true, 327 | "description": "Enables class code actions", 328 | "scope": "resource", 329 | "type": "boolean" 330 | }, 331 | "haskell.plugin.class.codeLensOn": { 332 | "default": true, 333 | "description": "Enables class code lenses", 334 | "scope": "resource", 335 | "type": "boolean" 336 | }, 337 | "haskell.plugin.eval.codeActionsOn": { 338 | "default": true, 339 | "description": "Enables eval code actions", 340 | "scope": "resource", 341 | "type": "boolean" 342 | }, 343 | "haskell.plugin.eval.codeLensOn": { 344 | "default": true, 345 | "description": "Enables eval code lenses", 346 | "scope": "resource", 347 | "type": "boolean" 348 | }, 349 | "haskell.plugin.eval.config.diff": { 350 | "default": true, 351 | "markdownDescription": "Enable the diff output (WAS/NOW) of eval lenses", 352 | "scope": "resource", 353 | "type": "boolean" 354 | }, 355 | "haskell.plugin.eval.config.exception": { 356 | "default": false, 357 | "markdownDescription": "Enable marking exceptions with `*** Exception:` similarly to doctest and GHCi.", 358 | "scope": "resource", 359 | "type": "boolean" 360 | }, 361 | "haskell.plugin.explicit-fields.codeActionsOn": { 362 | "default": true, 363 | "description": "Enables explicit-fields code actions", 364 | "scope": "resource", 365 | "type": "boolean" 366 | }, 367 | "haskell.plugin.explicit-fields.inlayHintsOn": { 368 | "default": true, 369 | "description": "Enables explicit-fields inlay hints", 370 | "scope": "resource", 371 | "type": "boolean" 372 | }, 373 | "haskell.plugin.explicit-fixity.globalOn": { 374 | "default": true, 375 | "description": "Enables explicit-fixity plugin", 376 | "scope": "resource", 377 | "type": "boolean" 378 | }, 379 | "haskell.plugin.fourmolu.config.external": { 380 | "default": false, 381 | "markdownDescription": "Call out to an external \"fourmolu\" executable, rather than using the bundled library.", 382 | "scope": "resource", 383 | "type": "boolean" 384 | }, 385 | "haskell.plugin.fourmolu.config.path": { 386 | "default": "fourmolu", 387 | "markdownDescription": "Set path to executable (for \"external\" mode).", 388 | "scope": "resource", 389 | "type": "string" 390 | }, 391 | "haskell.plugin.gadt.globalOn": { 392 | "default": true, 393 | "description": "Enables gadt plugin", 394 | "scope": "resource", 395 | "type": "boolean" 396 | }, 397 | "haskell.plugin.ghcide-code-actions-bindings.globalOn": { 398 | "default": true, 399 | "description": "Enables ghcide-code-actions-bindings plugin", 400 | "scope": "resource", 401 | "type": "boolean" 402 | }, 403 | "haskell.plugin.ghcide-code-actions-fill-holes.globalOn": { 404 | "default": true, 405 | "description": "Enables ghcide-code-actions-fill-holes plugin", 406 | "scope": "resource", 407 | "type": "boolean" 408 | }, 409 | "haskell.plugin.ghcide-code-actions-imports-exports.globalOn": { 410 | "default": true, 411 | "description": "Enables ghcide-code-actions-imports-exports plugin", 412 | "scope": "resource", 413 | "type": "boolean" 414 | }, 415 | "haskell.plugin.ghcide-code-actions-type-signatures.globalOn": { 416 | "default": true, 417 | "description": "Enables ghcide-code-actions-type-signatures plugin", 418 | "scope": "resource", 419 | "type": "boolean" 420 | }, 421 | "haskell.plugin.ghcide-completions.config.autoExtendOn": { 422 | "default": true, 423 | "markdownDescription": "Extends the import list automatically when completing a out-of-scope identifier", 424 | "scope": "resource", 425 | "type": "boolean" 426 | }, 427 | "haskell.plugin.ghcide-completions.config.snippetsOn": { 428 | "default": true, 429 | "markdownDescription": "Inserts snippets when using code completions", 430 | "scope": "resource", 431 | "type": "boolean" 432 | }, 433 | "haskell.plugin.ghcide-completions.globalOn": { 434 | "default": true, 435 | "description": "Enables ghcide-completions plugin", 436 | "scope": "resource", 437 | "type": "boolean" 438 | }, 439 | "haskell.plugin.ghcide-hover-and-symbols.hoverOn": { 440 | "default": true, 441 | "description": "Enables ghcide-hover-and-symbols hover", 442 | "scope": "resource", 443 | "type": "boolean" 444 | }, 445 | "haskell.plugin.ghcide-hover-and-symbols.symbolsOn": { 446 | "default": true, 447 | "description": "Enables ghcide-hover-and-symbols symbols", 448 | "scope": "resource", 449 | "type": "boolean" 450 | }, 451 | "haskell.plugin.ghcide-type-lenses.config.mode": { 452 | "default": "always", 453 | "description": "Control how type lenses are shown", 454 | "enum": [ 455 | "always", 456 | "exported", 457 | "diagnostics" 458 | ], 459 | "enumDescriptions": [ 460 | "Always displays type lenses of global bindings", 461 | "Only display type lenses of exported global bindings", 462 | "Follows error messages produced by GHC about missing signatures" 463 | ], 464 | "scope": "resource", 465 | "type": "string" 466 | }, 467 | "haskell.plugin.ghcide-type-lenses.globalOn": { 468 | "default": true, 469 | "description": "Enables ghcide-type-lenses plugin", 470 | "scope": "resource", 471 | "type": "boolean" 472 | }, 473 | "haskell.plugin.hlint.codeActionsOn": { 474 | "default": true, 475 | "description": "Enables hlint code actions", 476 | "scope": "resource", 477 | "type": "boolean" 478 | }, 479 | "haskell.plugin.hlint.config.flags": { 480 | "default": [], 481 | "markdownDescription": "Flags used by hlint", 482 | "scope": "resource", 483 | "type": "array" 484 | }, 485 | "haskell.plugin.hlint.diagnosticsOn": { 486 | "default": true, 487 | "description": "Enables hlint diagnostics", 488 | "scope": "resource", 489 | "type": "boolean" 490 | }, 491 | "haskell.plugin.importLens.codeActionsOn": { 492 | "default": true, 493 | "description": "Enables importLens code actions", 494 | "scope": "resource", 495 | "type": "boolean" 496 | }, 497 | "haskell.plugin.importLens.codeLensOn": { 498 | "default": true, 499 | "description": "Enables importLens code lenses", 500 | "scope": "resource", 501 | "type": "boolean" 502 | }, 503 | "haskell.plugin.importLens.inlayHintsOn": { 504 | "default": true, 505 | "description": "Enables importLens inlay hints", 506 | "scope": "resource", 507 | "type": "boolean" 508 | }, 509 | "haskell.plugin.moduleName.globalOn": { 510 | "default": true, 511 | "description": "Enables moduleName plugin", 512 | "scope": "resource", 513 | "type": "boolean" 514 | }, 515 | "haskell.plugin.ormolu.config.external": { 516 | "default": false, 517 | "markdownDescription": "Call out to an external \"ormolu\" executable, rather than using the bundled library", 518 | "scope": "resource", 519 | "type": "boolean" 520 | }, 521 | "haskell.plugin.overloaded-record-dot.globalOn": { 522 | "default": true, 523 | "description": "Enables overloaded-record-dot plugin", 524 | "scope": "resource", 525 | "type": "boolean" 526 | }, 527 | "haskell.plugin.pragmas-completion.globalOn": { 528 | "default": true, 529 | "description": "Enables pragmas-completion plugin", 530 | "scope": "resource", 531 | "type": "boolean" 532 | }, 533 | "haskell.plugin.pragmas-disable.globalOn": { 534 | "default": true, 535 | "description": "Enables pragmas-disable plugin", 536 | "scope": "resource", 537 | "type": "boolean" 538 | }, 539 | "haskell.plugin.pragmas-suggest.globalOn": { 540 | "default": true, 541 | "description": "Enables pragmas-suggest plugin", 542 | "scope": "resource", 543 | "type": "boolean" 544 | }, 545 | "haskell.plugin.qualifyImportedNames.globalOn": { 546 | "default": true, 547 | "description": "Enables qualifyImportedNames plugin", 548 | "scope": "resource", 549 | "type": "boolean" 550 | }, 551 | "haskell.plugin.rename.config.crossModule": { 552 | "default": false, 553 | "markdownDescription": "Enable experimental cross-module renaming", 554 | "scope": "resource", 555 | "type": "boolean" 556 | }, 557 | "haskell.plugin.rename.globalOn": { 558 | "default": true, 559 | "description": "Enables rename plugin", 560 | "scope": "resource", 561 | "type": "boolean" 562 | }, 563 | "haskell.plugin.retrie.globalOn": { 564 | "default": true, 565 | "description": "Enables retrie plugin", 566 | "scope": "resource", 567 | "type": "boolean" 568 | }, 569 | "haskell.plugin.semanticTokens.config.classMethodToken": { 570 | "default": "method", 571 | "description": "LSP semantic token type to use for typeclass methods", 572 | "enum": [ 573 | "namespace", 574 | "type", 575 | "class", 576 | "enum", 577 | "interface", 578 | "struct", 579 | "typeParameter", 580 | "parameter", 581 | "variable", 582 | "property", 583 | "enumMember", 584 | "event", 585 | "function", 586 | "method", 587 | "macro", 588 | "keyword", 589 | "modifier", 590 | "comment", 591 | "string", 592 | "number", 593 | "regexp", 594 | "operator", 595 | "decorator" 596 | ], 597 | "enumDescriptions": [ 598 | "LSP Semantic Token Type: namespace", 599 | "LSP Semantic Token Type: type", 600 | "LSP Semantic Token Type: class", 601 | "LSP Semantic Token Type: enum", 602 | "LSP Semantic Token Type: interface", 603 | "LSP Semantic Token Type: struct", 604 | "LSP Semantic Token Type: typeParameter", 605 | "LSP Semantic Token Type: parameter", 606 | "LSP Semantic Token Type: variable", 607 | "LSP Semantic Token Type: property", 608 | "LSP Semantic Token Type: enumMember", 609 | "LSP Semantic Token Type: event", 610 | "LSP Semantic Token Type: function", 611 | "LSP Semantic Token Type: method", 612 | "LSP Semantic Token Type: macro", 613 | "LSP Semantic Token Type: keyword", 614 | "LSP Semantic Token Type: modifier", 615 | "LSP Semantic Token Type: comment", 616 | "LSP Semantic Token Type: string", 617 | "LSP Semantic Token Type: number", 618 | "LSP Semantic Token Type: regexp", 619 | "LSP Semantic Token Type: operator", 620 | "LSP Semantic Token Type: decorator" 621 | ], 622 | "scope": "resource", 623 | "type": "string" 624 | }, 625 | "haskell.plugin.semanticTokens.config.classToken": { 626 | "default": "class", 627 | "description": "LSP semantic token type to use for typeclasses", 628 | "enum": [ 629 | "namespace", 630 | "type", 631 | "class", 632 | "enum", 633 | "interface", 634 | "struct", 635 | "typeParameter", 636 | "parameter", 637 | "variable", 638 | "property", 639 | "enumMember", 640 | "event", 641 | "function", 642 | "method", 643 | "macro", 644 | "keyword", 645 | "modifier", 646 | "comment", 647 | "string", 648 | "number", 649 | "regexp", 650 | "operator", 651 | "decorator" 652 | ], 653 | "enumDescriptions": [ 654 | "LSP Semantic Token Type: namespace", 655 | "LSP Semantic Token Type: type", 656 | "LSP Semantic Token Type: class", 657 | "LSP Semantic Token Type: enum", 658 | "LSP Semantic Token Type: interface", 659 | "LSP Semantic Token Type: struct", 660 | "LSP Semantic Token Type: typeParameter", 661 | "LSP Semantic Token Type: parameter", 662 | "LSP Semantic Token Type: variable", 663 | "LSP Semantic Token Type: property", 664 | "LSP Semantic Token Type: enumMember", 665 | "LSP Semantic Token Type: event", 666 | "LSP Semantic Token Type: function", 667 | "LSP Semantic Token Type: method", 668 | "LSP Semantic Token Type: macro", 669 | "LSP Semantic Token Type: keyword", 670 | "LSP Semantic Token Type: modifier", 671 | "LSP Semantic Token Type: comment", 672 | "LSP Semantic Token Type: string", 673 | "LSP Semantic Token Type: number", 674 | "LSP Semantic Token Type: regexp", 675 | "LSP Semantic Token Type: operator", 676 | "LSP Semantic Token Type: decorator" 677 | ], 678 | "scope": "resource", 679 | "type": "string" 680 | }, 681 | "haskell.plugin.semanticTokens.config.dataConstructorToken": { 682 | "default": "enumMember", 683 | "description": "LSP semantic token type to use for data constructors", 684 | "enum": [ 685 | "namespace", 686 | "type", 687 | "class", 688 | "enum", 689 | "interface", 690 | "struct", 691 | "typeParameter", 692 | "parameter", 693 | "variable", 694 | "property", 695 | "enumMember", 696 | "event", 697 | "function", 698 | "method", 699 | "macro", 700 | "keyword", 701 | "modifier", 702 | "comment", 703 | "string", 704 | "number", 705 | "regexp", 706 | "operator", 707 | "decorator" 708 | ], 709 | "enumDescriptions": [ 710 | "LSP Semantic Token Type: namespace", 711 | "LSP Semantic Token Type: type", 712 | "LSP Semantic Token Type: class", 713 | "LSP Semantic Token Type: enum", 714 | "LSP Semantic Token Type: interface", 715 | "LSP Semantic Token Type: struct", 716 | "LSP Semantic Token Type: typeParameter", 717 | "LSP Semantic Token Type: parameter", 718 | "LSP Semantic Token Type: variable", 719 | "LSP Semantic Token Type: property", 720 | "LSP Semantic Token Type: enumMember", 721 | "LSP Semantic Token Type: event", 722 | "LSP Semantic Token Type: function", 723 | "LSP Semantic Token Type: method", 724 | "LSP Semantic Token Type: macro", 725 | "LSP Semantic Token Type: keyword", 726 | "LSP Semantic Token Type: modifier", 727 | "LSP Semantic Token Type: comment", 728 | "LSP Semantic Token Type: string", 729 | "LSP Semantic Token Type: number", 730 | "LSP Semantic Token Type: regexp", 731 | "LSP Semantic Token Type: operator", 732 | "LSP Semantic Token Type: decorator" 733 | ], 734 | "scope": "resource", 735 | "type": "string" 736 | }, 737 | "haskell.plugin.semanticTokens.config.functionToken": { 738 | "default": "function", 739 | "description": "LSP semantic token type to use for functions", 740 | "enum": [ 741 | "namespace", 742 | "type", 743 | "class", 744 | "enum", 745 | "interface", 746 | "struct", 747 | "typeParameter", 748 | "parameter", 749 | "variable", 750 | "property", 751 | "enumMember", 752 | "event", 753 | "function", 754 | "method", 755 | "macro", 756 | "keyword", 757 | "modifier", 758 | "comment", 759 | "string", 760 | "number", 761 | "regexp", 762 | "operator", 763 | "decorator" 764 | ], 765 | "enumDescriptions": [ 766 | "LSP Semantic Token Type: namespace", 767 | "LSP Semantic Token Type: type", 768 | "LSP Semantic Token Type: class", 769 | "LSP Semantic Token Type: enum", 770 | "LSP Semantic Token Type: interface", 771 | "LSP Semantic Token Type: struct", 772 | "LSP Semantic Token Type: typeParameter", 773 | "LSP Semantic Token Type: parameter", 774 | "LSP Semantic Token Type: variable", 775 | "LSP Semantic Token Type: property", 776 | "LSP Semantic Token Type: enumMember", 777 | "LSP Semantic Token Type: event", 778 | "LSP Semantic Token Type: function", 779 | "LSP Semantic Token Type: method", 780 | "LSP Semantic Token Type: macro", 781 | "LSP Semantic Token Type: keyword", 782 | "LSP Semantic Token Type: modifier", 783 | "LSP Semantic Token Type: comment", 784 | "LSP Semantic Token Type: string", 785 | "LSP Semantic Token Type: number", 786 | "LSP Semantic Token Type: regexp", 787 | "LSP Semantic Token Type: operator", 788 | "LSP Semantic Token Type: decorator" 789 | ], 790 | "scope": "resource", 791 | "type": "string" 792 | }, 793 | "haskell.plugin.semanticTokens.config.moduleToken": { 794 | "default": "namespace", 795 | "description": "LSP semantic token type to use for modules", 796 | "enum": [ 797 | "namespace", 798 | "type", 799 | "class", 800 | "enum", 801 | "interface", 802 | "struct", 803 | "typeParameter", 804 | "parameter", 805 | "variable", 806 | "property", 807 | "enumMember", 808 | "event", 809 | "function", 810 | "method", 811 | "macro", 812 | "keyword", 813 | "modifier", 814 | "comment", 815 | "string", 816 | "number", 817 | "regexp", 818 | "operator", 819 | "decorator" 820 | ], 821 | "enumDescriptions": [ 822 | "LSP Semantic Token Type: namespace", 823 | "LSP Semantic Token Type: type", 824 | "LSP Semantic Token Type: class", 825 | "LSP Semantic Token Type: enum", 826 | "LSP Semantic Token Type: interface", 827 | "LSP Semantic Token Type: struct", 828 | "LSP Semantic Token Type: typeParameter", 829 | "LSP Semantic Token Type: parameter", 830 | "LSP Semantic Token Type: variable", 831 | "LSP Semantic Token Type: property", 832 | "LSP Semantic Token Type: enumMember", 833 | "LSP Semantic Token Type: event", 834 | "LSP Semantic Token Type: function", 835 | "LSP Semantic Token Type: method", 836 | "LSP Semantic Token Type: macro", 837 | "LSP Semantic Token Type: keyword", 838 | "LSP Semantic Token Type: modifier", 839 | "LSP Semantic Token Type: comment", 840 | "LSP Semantic Token Type: string", 841 | "LSP Semantic Token Type: number", 842 | "LSP Semantic Token Type: regexp", 843 | "LSP Semantic Token Type: operator", 844 | "LSP Semantic Token Type: decorator" 845 | ], 846 | "scope": "resource", 847 | "type": "string" 848 | }, 849 | "haskell.plugin.semanticTokens.config.operatorToken": { 850 | "default": "operator", 851 | "description": "LSP semantic token type to use for operators", 852 | "enum": [ 853 | "namespace", 854 | "type", 855 | "class", 856 | "enum", 857 | "interface", 858 | "struct", 859 | "typeParameter", 860 | "parameter", 861 | "variable", 862 | "property", 863 | "enumMember", 864 | "event", 865 | "function", 866 | "method", 867 | "macro", 868 | "keyword", 869 | "modifier", 870 | "comment", 871 | "string", 872 | "number", 873 | "regexp", 874 | "operator", 875 | "decorator" 876 | ], 877 | "enumDescriptions": [ 878 | "LSP Semantic Token Type: namespace", 879 | "LSP Semantic Token Type: type", 880 | "LSP Semantic Token Type: class", 881 | "LSP Semantic Token Type: enum", 882 | "LSP Semantic Token Type: interface", 883 | "LSP Semantic Token Type: struct", 884 | "LSP Semantic Token Type: typeParameter", 885 | "LSP Semantic Token Type: parameter", 886 | "LSP Semantic Token Type: variable", 887 | "LSP Semantic Token Type: property", 888 | "LSP Semantic Token Type: enumMember", 889 | "LSP Semantic Token Type: event", 890 | "LSP Semantic Token Type: function", 891 | "LSP Semantic Token Type: method", 892 | "LSP Semantic Token Type: macro", 893 | "LSP Semantic Token Type: keyword", 894 | "LSP Semantic Token Type: modifier", 895 | "LSP Semantic Token Type: comment", 896 | "LSP Semantic Token Type: string", 897 | "LSP Semantic Token Type: number", 898 | "LSP Semantic Token Type: regexp", 899 | "LSP Semantic Token Type: operator", 900 | "LSP Semantic Token Type: decorator" 901 | ], 902 | "scope": "resource", 903 | "type": "string" 904 | }, 905 | "haskell.plugin.semanticTokens.config.patternSynonymToken": { 906 | "default": "macro", 907 | "description": "LSP semantic token type to use for pattern synonyms", 908 | "enum": [ 909 | "namespace", 910 | "type", 911 | "class", 912 | "enum", 913 | "interface", 914 | "struct", 915 | "typeParameter", 916 | "parameter", 917 | "variable", 918 | "property", 919 | "enumMember", 920 | "event", 921 | "function", 922 | "method", 923 | "macro", 924 | "keyword", 925 | "modifier", 926 | "comment", 927 | "string", 928 | "number", 929 | "regexp", 930 | "operator", 931 | "decorator" 932 | ], 933 | "enumDescriptions": [ 934 | "LSP Semantic Token Type: namespace", 935 | "LSP Semantic Token Type: type", 936 | "LSP Semantic Token Type: class", 937 | "LSP Semantic Token Type: enum", 938 | "LSP Semantic Token Type: interface", 939 | "LSP Semantic Token Type: struct", 940 | "LSP Semantic Token Type: typeParameter", 941 | "LSP Semantic Token Type: parameter", 942 | "LSP Semantic Token Type: variable", 943 | "LSP Semantic Token Type: property", 944 | "LSP Semantic Token Type: enumMember", 945 | "LSP Semantic Token Type: event", 946 | "LSP Semantic Token Type: function", 947 | "LSP Semantic Token Type: method", 948 | "LSP Semantic Token Type: macro", 949 | "LSP Semantic Token Type: keyword", 950 | "LSP Semantic Token Type: modifier", 951 | "LSP Semantic Token Type: comment", 952 | "LSP Semantic Token Type: string", 953 | "LSP Semantic Token Type: number", 954 | "LSP Semantic Token Type: regexp", 955 | "LSP Semantic Token Type: operator", 956 | "LSP Semantic Token Type: decorator" 957 | ], 958 | "scope": "resource", 959 | "type": "string" 960 | }, 961 | "haskell.plugin.semanticTokens.config.recordFieldToken": { 962 | "default": "property", 963 | "description": "LSP semantic token type to use for record fields", 964 | "enum": [ 965 | "namespace", 966 | "type", 967 | "class", 968 | "enum", 969 | "interface", 970 | "struct", 971 | "typeParameter", 972 | "parameter", 973 | "variable", 974 | "property", 975 | "enumMember", 976 | "event", 977 | "function", 978 | "method", 979 | "macro", 980 | "keyword", 981 | "modifier", 982 | "comment", 983 | "string", 984 | "number", 985 | "regexp", 986 | "operator", 987 | "decorator" 988 | ], 989 | "enumDescriptions": [ 990 | "LSP Semantic Token Type: namespace", 991 | "LSP Semantic Token Type: type", 992 | "LSP Semantic Token Type: class", 993 | "LSP Semantic Token Type: enum", 994 | "LSP Semantic Token Type: interface", 995 | "LSP Semantic Token Type: struct", 996 | "LSP Semantic Token Type: typeParameter", 997 | "LSP Semantic Token Type: parameter", 998 | "LSP Semantic Token Type: variable", 999 | "LSP Semantic Token Type: property", 1000 | "LSP Semantic Token Type: enumMember", 1001 | "LSP Semantic Token Type: event", 1002 | "LSP Semantic Token Type: function", 1003 | "LSP Semantic Token Type: method", 1004 | "LSP Semantic Token Type: macro", 1005 | "LSP Semantic Token Type: keyword", 1006 | "LSP Semantic Token Type: modifier", 1007 | "LSP Semantic Token Type: comment", 1008 | "LSP Semantic Token Type: string", 1009 | "LSP Semantic Token Type: number", 1010 | "LSP Semantic Token Type: regexp", 1011 | "LSP Semantic Token Type: operator", 1012 | "LSP Semantic Token Type: decorator" 1013 | ], 1014 | "scope": "resource", 1015 | "type": "string" 1016 | }, 1017 | "haskell.plugin.semanticTokens.config.typeConstructorToken": { 1018 | "default": "enum", 1019 | "description": "LSP semantic token type to use for type constructors", 1020 | "enum": [ 1021 | "namespace", 1022 | "type", 1023 | "class", 1024 | "enum", 1025 | "interface", 1026 | "struct", 1027 | "typeParameter", 1028 | "parameter", 1029 | "variable", 1030 | "property", 1031 | "enumMember", 1032 | "event", 1033 | "function", 1034 | "method", 1035 | "macro", 1036 | "keyword", 1037 | "modifier", 1038 | "comment", 1039 | "string", 1040 | "number", 1041 | "regexp", 1042 | "operator", 1043 | "decorator" 1044 | ], 1045 | "enumDescriptions": [ 1046 | "LSP Semantic Token Type: namespace", 1047 | "LSP Semantic Token Type: type", 1048 | "LSP Semantic Token Type: class", 1049 | "LSP Semantic Token Type: enum", 1050 | "LSP Semantic Token Type: interface", 1051 | "LSP Semantic Token Type: struct", 1052 | "LSP Semantic Token Type: typeParameter", 1053 | "LSP Semantic Token Type: parameter", 1054 | "LSP Semantic Token Type: variable", 1055 | "LSP Semantic Token Type: property", 1056 | "LSP Semantic Token Type: enumMember", 1057 | "LSP Semantic Token Type: event", 1058 | "LSP Semantic Token Type: function", 1059 | "LSP Semantic Token Type: method", 1060 | "LSP Semantic Token Type: macro", 1061 | "LSP Semantic Token Type: keyword", 1062 | "LSP Semantic Token Type: modifier", 1063 | "LSP Semantic Token Type: comment", 1064 | "LSP Semantic Token Type: string", 1065 | "LSP Semantic Token Type: number", 1066 | "LSP Semantic Token Type: regexp", 1067 | "LSP Semantic Token Type: operator", 1068 | "LSP Semantic Token Type: decorator" 1069 | ], 1070 | "scope": "resource", 1071 | "type": "string" 1072 | }, 1073 | "haskell.plugin.semanticTokens.config.typeFamilyToken": { 1074 | "default": "interface", 1075 | "description": "LSP semantic token type to use for type families", 1076 | "enum": [ 1077 | "namespace", 1078 | "type", 1079 | "class", 1080 | "enum", 1081 | "interface", 1082 | "struct", 1083 | "typeParameter", 1084 | "parameter", 1085 | "variable", 1086 | "property", 1087 | "enumMember", 1088 | "event", 1089 | "function", 1090 | "method", 1091 | "macro", 1092 | "keyword", 1093 | "modifier", 1094 | "comment", 1095 | "string", 1096 | "number", 1097 | "regexp", 1098 | "operator", 1099 | "decorator" 1100 | ], 1101 | "enumDescriptions": [ 1102 | "LSP Semantic Token Type: namespace", 1103 | "LSP Semantic Token Type: type", 1104 | "LSP Semantic Token Type: class", 1105 | "LSP Semantic Token Type: enum", 1106 | "LSP Semantic Token Type: interface", 1107 | "LSP Semantic Token Type: struct", 1108 | "LSP Semantic Token Type: typeParameter", 1109 | "LSP Semantic Token Type: parameter", 1110 | "LSP Semantic Token Type: variable", 1111 | "LSP Semantic Token Type: property", 1112 | "LSP Semantic Token Type: enumMember", 1113 | "LSP Semantic Token Type: event", 1114 | "LSP Semantic Token Type: function", 1115 | "LSP Semantic Token Type: method", 1116 | "LSP Semantic Token Type: macro", 1117 | "LSP Semantic Token Type: keyword", 1118 | "LSP Semantic Token Type: modifier", 1119 | "LSP Semantic Token Type: comment", 1120 | "LSP Semantic Token Type: string", 1121 | "LSP Semantic Token Type: number", 1122 | "LSP Semantic Token Type: regexp", 1123 | "LSP Semantic Token Type: operator", 1124 | "LSP Semantic Token Type: decorator" 1125 | ], 1126 | "scope": "resource", 1127 | "type": "string" 1128 | }, 1129 | "haskell.plugin.semanticTokens.config.typeSynonymToken": { 1130 | "default": "type", 1131 | "description": "LSP semantic token type to use for type synonyms", 1132 | "enum": [ 1133 | "namespace", 1134 | "type", 1135 | "class", 1136 | "enum", 1137 | "interface", 1138 | "struct", 1139 | "typeParameter", 1140 | "parameter", 1141 | "variable", 1142 | "property", 1143 | "enumMember", 1144 | "event", 1145 | "function", 1146 | "method", 1147 | "macro", 1148 | "keyword", 1149 | "modifier", 1150 | "comment", 1151 | "string", 1152 | "number", 1153 | "regexp", 1154 | "operator", 1155 | "decorator" 1156 | ], 1157 | "enumDescriptions": [ 1158 | "LSP Semantic Token Type: namespace", 1159 | "LSP Semantic Token Type: type", 1160 | "LSP Semantic Token Type: class", 1161 | "LSP Semantic Token Type: enum", 1162 | "LSP Semantic Token Type: interface", 1163 | "LSP Semantic Token Type: struct", 1164 | "LSP Semantic Token Type: typeParameter", 1165 | "LSP Semantic Token Type: parameter", 1166 | "LSP Semantic Token Type: variable", 1167 | "LSP Semantic Token Type: property", 1168 | "LSP Semantic Token Type: enumMember", 1169 | "LSP Semantic Token Type: event", 1170 | "LSP Semantic Token Type: function", 1171 | "LSP Semantic Token Type: method", 1172 | "LSP Semantic Token Type: macro", 1173 | "LSP Semantic Token Type: keyword", 1174 | "LSP Semantic Token Type: modifier", 1175 | "LSP Semantic Token Type: comment", 1176 | "LSP Semantic Token Type: string", 1177 | "LSP Semantic Token Type: number", 1178 | "LSP Semantic Token Type: regexp", 1179 | "LSP Semantic Token Type: operator", 1180 | "LSP Semantic Token Type: decorator" 1181 | ], 1182 | "scope": "resource", 1183 | "type": "string" 1184 | }, 1185 | "haskell.plugin.semanticTokens.config.typeVariableToken": { 1186 | "default": "typeParameter", 1187 | "description": "LSP semantic token type to use for type variables", 1188 | "enum": [ 1189 | "namespace", 1190 | "type", 1191 | "class", 1192 | "enum", 1193 | "interface", 1194 | "struct", 1195 | "typeParameter", 1196 | "parameter", 1197 | "variable", 1198 | "property", 1199 | "enumMember", 1200 | "event", 1201 | "function", 1202 | "method", 1203 | "macro", 1204 | "keyword", 1205 | "modifier", 1206 | "comment", 1207 | "string", 1208 | "number", 1209 | "regexp", 1210 | "operator", 1211 | "decorator" 1212 | ], 1213 | "enumDescriptions": [ 1214 | "LSP Semantic Token Type: namespace", 1215 | "LSP Semantic Token Type: type", 1216 | "LSP Semantic Token Type: class", 1217 | "LSP Semantic Token Type: enum", 1218 | "LSP Semantic Token Type: interface", 1219 | "LSP Semantic Token Type: struct", 1220 | "LSP Semantic Token Type: typeParameter", 1221 | "LSP Semantic Token Type: parameter", 1222 | "LSP Semantic Token Type: variable", 1223 | "LSP Semantic Token Type: property", 1224 | "LSP Semantic Token Type: enumMember", 1225 | "LSP Semantic Token Type: event", 1226 | "LSP Semantic Token Type: function", 1227 | "LSP Semantic Token Type: method", 1228 | "LSP Semantic Token Type: macro", 1229 | "LSP Semantic Token Type: keyword", 1230 | "LSP Semantic Token Type: modifier", 1231 | "LSP Semantic Token Type: comment", 1232 | "LSP Semantic Token Type: string", 1233 | "LSP Semantic Token Type: number", 1234 | "LSP Semantic Token Type: regexp", 1235 | "LSP Semantic Token Type: operator", 1236 | "LSP Semantic Token Type: decorator" 1237 | ], 1238 | "scope": "resource", 1239 | "type": "string" 1240 | }, 1241 | "haskell.plugin.semanticTokens.config.variableToken": { 1242 | "default": "variable", 1243 | "description": "LSP semantic token type to use for variables", 1244 | "enum": [ 1245 | "namespace", 1246 | "type", 1247 | "class", 1248 | "enum", 1249 | "interface", 1250 | "struct", 1251 | "typeParameter", 1252 | "parameter", 1253 | "variable", 1254 | "property", 1255 | "enumMember", 1256 | "event", 1257 | "function", 1258 | "method", 1259 | "macro", 1260 | "keyword", 1261 | "modifier", 1262 | "comment", 1263 | "string", 1264 | "number", 1265 | "regexp", 1266 | "operator", 1267 | "decorator" 1268 | ], 1269 | "enumDescriptions": [ 1270 | "LSP Semantic Token Type: namespace", 1271 | "LSP Semantic Token Type: type", 1272 | "LSP Semantic Token Type: class", 1273 | "LSP Semantic Token Type: enum", 1274 | "LSP Semantic Token Type: interface", 1275 | "LSP Semantic Token Type: struct", 1276 | "LSP Semantic Token Type: typeParameter", 1277 | "LSP Semantic Token Type: parameter", 1278 | "LSP Semantic Token Type: variable", 1279 | "LSP Semantic Token Type: property", 1280 | "LSP Semantic Token Type: enumMember", 1281 | "LSP Semantic Token Type: event", 1282 | "LSP Semantic Token Type: function", 1283 | "LSP Semantic Token Type: method", 1284 | "LSP Semantic Token Type: macro", 1285 | "LSP Semantic Token Type: keyword", 1286 | "LSP Semantic Token Type: modifier", 1287 | "LSP Semantic Token Type: comment", 1288 | "LSP Semantic Token Type: string", 1289 | "LSP Semantic Token Type: number", 1290 | "LSP Semantic Token Type: regexp", 1291 | "LSP Semantic Token Type: operator", 1292 | "LSP Semantic Token Type: decorator" 1293 | ], 1294 | "scope": "resource", 1295 | "type": "string" 1296 | }, 1297 | "haskell.plugin.semanticTokens.globalOn": { 1298 | "default": false, 1299 | "description": "Enables semanticTokens plugin", 1300 | "scope": "resource", 1301 | "type": "boolean" 1302 | }, 1303 | "haskell.plugin.splice.globalOn": { 1304 | "default": true, 1305 | "description": "Enables splice plugin", 1306 | "scope": "resource", 1307 | "type": "boolean" 1308 | }, 1309 | "haskell.plugin.stan.globalOn": { 1310 | "default": false, 1311 | "description": "Enables stan plugin", 1312 | "scope": "resource", 1313 | "type": "boolean" 1314 | } 1315 | } 1316 | }, 1317 | "commands": [ 1318 | { 1319 | "command": "haskell.commands.restartExtension", 1320 | "title": "Haskell: Restart vscode-haskell extension", 1321 | "description": "Restart the vscode-haskell extension. Reloads configuration." 1322 | }, 1323 | { 1324 | "command": "haskell.commands.restartServer", 1325 | "title": "Haskell: Restart Haskell LSP server", 1326 | "description": "Restart the Haskell LSP server" 1327 | }, 1328 | { 1329 | "command": "haskell.commands.startServer", 1330 | "title": "Haskell: Start Haskell LSP server", 1331 | "description": "Start the Haskell LSP server" 1332 | }, 1333 | { 1334 | "command": "haskell.commands.stopServer", 1335 | "title": "Haskell: Stop Haskell LSP server", 1336 | "description": "Stop the Haskell LSP server" 1337 | } 1338 | ] 1339 | }, 1340 | "scripts": { 1341 | "vscode:prepublish": "webpack --mode production", 1342 | "webpack": "webpack --mode none", 1343 | "watch": "webpack --mode development --watch", 1344 | "lint": "eslint -c eslint.config.mjs src", 1345 | "lint-fix": "eslint --fix -c eslint.config.mjs src", 1346 | "push-tag": "git tag -a $npm_package_version -m \"Version $npm_package_version\" && git push origin $npm_package_version", 1347 | "pretest": "tsc --alwaysStrict -p ./", 1348 | "format": "prettier . --write", 1349 | "test": "vscode-test" 1350 | }, 1351 | "husky": { 1352 | "hooks": { 1353 | "pre-commit": "pretty-quick --staged" 1354 | } 1355 | }, 1356 | "devDependencies": { 1357 | "@eslint/js": "^9.37.0", 1358 | "@types/mocha": "^10.0.10", 1359 | "@types/node": "^22.15.29", 1360 | "@types/vscode": "^1.102.0", 1361 | "@types/which": "^3.0.4", 1362 | "@typescript-eslint/eslint-plugin": "^8.29.0", 1363 | "@typescript-eslint/parser": "^8.45.0", 1364 | "@vscode/test-cli": "^0.0.11", 1365 | "@vscode/test-electron": "^2.5.2", 1366 | "eslint": "^9.37.0", 1367 | "eslint-webpack-plugin": "^5.0.0", 1368 | "glob": "^11.1.0", 1369 | "globals": "^16.2.0", 1370 | "husky": "^9.1.7", 1371 | "mocha": "^11.1.0", 1372 | "prettier": "^3.5.3", 1373 | "ts-loader": "^9.5.2", 1374 | "typescript": "^5.8.3", 1375 | "typescript-eslint": "^8.45.0", 1376 | "webpack": "^5.98.0", 1377 | "webpack-cli": "^6.0.1" 1378 | }, 1379 | "extensionDependencies": [ 1380 | "justusadam.language-haskell" 1381 | ], 1382 | "dependencies": { 1383 | "ts-pattern": "^5.7.0", 1384 | "vscode-languageclient": "9.0.1", 1385 | "which": "^5.0.0" 1386 | } 1387 | } 1388 | -------------------------------------------------------------------------------- /src/commands/constants.ts: -------------------------------------------------------------------------------- 1 | export const RestartExtensionCommandName = 'haskell.commands.restartExtension'; 2 | export const RestartServerCommandName = 'haskell.commands.restartServer'; 3 | export const StartServerCommandName = 'haskell.commands.startServer'; 4 | export const StopServerCommandName = 'haskell.commands.stopServer'; 5 | export const OpenLogsCommandName = 'haskell.commands.openLogs'; 6 | export const ShowExtensionVersions = 'haskell.commands.showVersions'; 7 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { OutputChannel, Uri, window, WorkspaceConfiguration, WorkspaceFolder } from 'vscode'; 2 | import { expandHomeDir, IEnvVars } from './utils'; 3 | import * as path from 'path'; 4 | import { Logger } from 'vscode-languageclient'; 5 | import { ExtensionLogger } from './logger'; 6 | import { GHCupConfig } from './ghcup'; 7 | 8 | export type LogLevel = 'off' | 'messages' | 'verbose'; 9 | export type ClientLogLevel = 'off' | 'error' | 'info' | 'debug'; 10 | 11 | export type Config = { 12 | /** 13 | * Unique name per workspace folder (useful for multi-root workspaces). 14 | */ 15 | langName: string; 16 | logLevel: LogLevel; 17 | clientLogLevel: ClientLogLevel; 18 | logFilePath?: string; 19 | workingDir: string; 20 | outputChannel: OutputChannel; 21 | serverArgs: string[]; 22 | serverEnvironment: IEnvVars; 23 | ghcupConfig: GHCupConfig; 24 | }; 25 | 26 | export function initConfig(workspaceConfig: WorkspaceConfiguration, uri: Uri, folder?: WorkspaceFolder): Config { 27 | // Set a unique name per workspace folder (useful for multi-root workspaces). 28 | const langName = 'Haskell' + (folder ? ` (${folder.name})` : ''); 29 | const currentWorkingDir = folder ? folder.uri.fsPath : path.dirname(uri.fsPath); 30 | 31 | const logLevel = getLogLevel(workspaceConfig); 32 | const clientLogLevel = getClientLogLevel(workspaceConfig); 33 | 34 | const logFile = getLogFile(workspaceConfig); 35 | const logFilePath = resolveLogFilePath(logFile, currentWorkingDir); 36 | 37 | const outputChannel: OutputChannel = window.createOutputChannel(langName); 38 | const serverArgs = getServerArgs(workspaceConfig, logLevel, logFilePath); 39 | 40 | return { 41 | langName: langName, 42 | logLevel: logLevel, 43 | clientLogLevel: clientLogLevel, 44 | logFilePath: logFilePath, 45 | workingDir: currentWorkingDir, 46 | outputChannel: outputChannel, 47 | serverArgs: serverArgs, 48 | serverEnvironment: workspaceConfig.serverEnvironment, 49 | ghcupConfig: { 50 | metadataUrl: workspaceConfig.metadataURL as string, 51 | upgradeGHCup: workspaceConfig.get('upgradeGHCup') as boolean, 52 | executablePath: workspaceConfig.get('ghcupExecutablePath') as string, 53 | }, 54 | }; 55 | } 56 | 57 | export function initLoggerFromConfig(config: Config): ExtensionLogger { 58 | return new ExtensionLogger('client', config.clientLogLevel, config.outputChannel, config.logFilePath); 59 | } 60 | 61 | export function logConfig(logger: Logger, config: Config) { 62 | if (config.logFilePath) { 63 | logger.info(`Writing client log to file ${config.logFilePath}`); 64 | } 65 | logger.log('Environment variables:'); 66 | Object.entries(process.env).forEach(([key, value]: [string, string | undefined]) => { 67 | // only list environment variables that we actually care about. 68 | // this makes it safe for users to just paste the logs to whoever, 69 | // and avoids leaking secrets. 70 | if (['PATH'].includes(key)) { 71 | logger.log(` ${key}: ${value}`); 72 | } 73 | }); 74 | } 75 | 76 | function getLogFile(workspaceConfig: WorkspaceConfiguration) { 77 | const logFile_: unknown = workspaceConfig.logFile; 78 | let logFile: string | undefined; 79 | if (typeof logFile_ === 'string') { 80 | logFile = logFile_ !== '' ? logFile_ : undefined; 81 | } 82 | return logFile; 83 | } 84 | 85 | function getClientLogLevel(workspaceConfig: WorkspaceConfiguration): ClientLogLevel { 86 | const clientLogLevel_: unknown = workspaceConfig.trace.client; 87 | let clientLogLevel; 88 | if (typeof clientLogLevel_ === 'string') { 89 | switch (clientLogLevel_) { 90 | case 'off': 91 | case 'error': 92 | case 'info': 93 | case 'debug': 94 | clientLogLevel = clientLogLevel_; 95 | break; 96 | default: 97 | throw new Error("Option \"haskell.trace.client\" is expected to be one of 'off', 'error', 'info', 'debug'."); 98 | } 99 | } else { 100 | throw new Error('Option "haskell.trace.client" is expected to be a string'); 101 | } 102 | return clientLogLevel; 103 | } 104 | 105 | function getLogLevel(workspaceConfig: WorkspaceConfiguration): LogLevel { 106 | const logLevel_: unknown = workspaceConfig.trace.server; 107 | let logLevel; 108 | if (typeof logLevel_ === 'string') { 109 | switch (logLevel_) { 110 | case 'off': 111 | case 'messages': 112 | case 'verbose': 113 | logLevel = logLevel_; 114 | break; 115 | default: 116 | throw new Error("Option \"haskell.trace.server\" is expected to be one of 'off', 'messages', 'verbose'."); 117 | } 118 | } else { 119 | throw new Error('Option "haskell.trace.server" is expected to be a string'); 120 | } 121 | return logLevel; 122 | } 123 | 124 | function resolveLogFilePath(logFile: string | undefined, currentWorkingDir: string): string | undefined { 125 | return logFile !== undefined ? path.resolve(currentWorkingDir, expandHomeDir(logFile)) : undefined; 126 | } 127 | 128 | function getServerArgs(workspaceConfig: WorkspaceConfiguration, logLevel: LogLevel, logFilePath?: string): string[] { 129 | const serverArgs = ['--lsp'] 130 | .concat(logLevel === 'messages' ? ['-d'] : []) 131 | .concat(logFilePath !== undefined ? ['-l', logFilePath] : []); 132 | 133 | const rawExtraArgs: unknown = workspaceConfig.serverExtraArgs; 134 | if (typeof rawExtraArgs === 'string' && rawExtraArgs !== '') { 135 | const e = rawExtraArgs.split(' '); 136 | serverArgs.push(...e); 137 | } 138 | 139 | // We don't want empty strings in our args 140 | return serverArgs.map((x) => x.trim()).filter((x) => x !== ''); 141 | } 142 | -------------------------------------------------------------------------------- /src/docsBrowser.ts: -------------------------------------------------------------------------------- 1 | import { dirname } from 'path'; 2 | import { 3 | CancellationToken, 4 | commands, 5 | CompletionContext, 6 | CompletionItem, 7 | CompletionList, 8 | Disposable, 9 | env, 10 | Hover, 11 | MarkdownString, 12 | MarkedString, 13 | Position, 14 | ProviderResult, 15 | TextDocument, 16 | Uri, 17 | ViewColumn, 18 | window, 19 | workspace, 20 | } from 'vscode'; 21 | import { ProvideCompletionItemsSignature, ProvideHoverSignature } from 'vscode-languageclient'; 22 | 23 | async function showDocumentation({ 24 | title, 25 | localPath, 26 | hackageUri, 27 | }: { 28 | title: string; 29 | localPath: string; 30 | hackageUri: string; 31 | }) { 32 | const arr = localPath.match(/([^/]+)\.[^.]+$/); 33 | const ttl = arr !== null && arr.length === 2 ? arr[1].replace(/-/gi, '.') : title; 34 | const documentationDirectory = dirname(localPath); 35 | let panel; 36 | try { 37 | const docUri = Uri.parse(documentationDirectory); 38 | 39 | // Make sure to use Uri.parse here, as path will already have 'file:///' in it 40 | panel = window.createWebviewPanel('haskell.showDocumentationPanel', ttl, ViewColumn.Beside, { 41 | localResourceRoots: [docUri], 42 | enableFindWidget: true, 43 | enableCommandUris: true, 44 | enableScripts: true, 45 | }); 46 | 47 | const encoded = encodeURIComponent(JSON.stringify({ hackageUri, inWebView: true })); 48 | const hackageCmd = 'command:haskell.openDocumentationOnHackage?' + encoded; 49 | 50 | const bytes = await workspace.fs.readFile(Uri.parse(localPath)); 51 | 52 | const addBase = ` 53 | 54 | `; 55 | 56 | panel.webview.html = ` 57 | 58 | ${addBase} 59 | 60 | 61 | ${bytes.toString()} 62 | 63 | 64 | `; 65 | } catch (e) { 66 | if (e instanceof Error) { 67 | await window.showErrorMessage(e.message); 68 | } 69 | } 70 | return panel; 71 | } 72 | 73 | // registers the browser in VSCode infrastructure 74 | export function registerDocsBrowser(): Disposable { 75 | return commands.registerCommand('haskell.showDocumentation', showDocumentation); 76 | } 77 | 78 | async function openDocumentationOnHackage({ 79 | hackageUri, 80 | inWebView = false, 81 | }: { 82 | hackageUri: string; 83 | inWebView: boolean; 84 | }) { 85 | try { 86 | // open on Hackage and close the original webview in VS code 87 | await env.openExternal(Uri.parse(hackageUri)); 88 | if (inWebView) { 89 | await commands.executeCommand('workbench.action.closeActiveEditor'); 90 | } 91 | } catch (e) { 92 | if (e instanceof Error) { 93 | await window.showErrorMessage(e.message); 94 | } 95 | } 96 | } 97 | 98 | export function registerDocsOpenOnHackage(): Disposable { 99 | return commands.registerCommand('haskell.openDocumentationOnHackage', openDocumentationOnHackage); 100 | } 101 | 102 | export function hoverLinksMiddlewareHook( 103 | document: TextDocument, 104 | position: Position, 105 | token: CancellationToken, 106 | next: ProvideHoverSignature, 107 | ): ProviderResult { 108 | const res = next(document, position, token); 109 | return Promise.resolve(res).then((r) => { 110 | if (r !== null && r !== undefined) { 111 | r.contents = r.contents.map(processLink); 112 | } 113 | return r; 114 | }); 115 | } 116 | 117 | export function completionLinksMiddlewareHook( 118 | document: TextDocument, 119 | position: Position, 120 | context: CompletionContext, 121 | token: CancellationToken, 122 | next: ProvideCompletionItemsSignature, 123 | ): ProviderResult { 124 | const res = next(document, position, context, token); 125 | 126 | function processCI(ci: CompletionItem): void { 127 | if (ci.documentation) { 128 | ci.documentation = processLink(ci.documentation); 129 | } 130 | } 131 | 132 | return Promise.resolve(res).then((r) => { 133 | if (r instanceof Array) { 134 | r.forEach(processCI); 135 | } else if (r) { 136 | r.items.forEach(processCI); 137 | } 138 | return r; 139 | }); 140 | } 141 | 142 | function processLink(ms: MarkdownString | MarkedString): string | MarkdownString { 143 | const openDocsInHackage = workspace.getConfiguration('haskell').get('openDocumentationInHackage'); 144 | const openSourceInHackage = workspace.getConfiguration('haskell').get('openSourceInHackage'); 145 | function transform(s: string): string { 146 | return s.replace( 147 | /\[(.+)\]\((file:.+\/doc\/(?:.*html\/libraries\/)?([^/]+)\/(?:.*\/)?(.+\.html#?.*))\)/gi, 148 | (_all, title, localPath, packageName, fileAndAnchor) => { 149 | let hackageUri: string; 150 | if (title === 'Documentation') { 151 | hackageUri = `https://hackage.haskell.org/package/${packageName}/docs/${fileAndAnchor}`; 152 | const encoded = encodeURIComponent(JSON.stringify({ title, localPath, hackageUri })); 153 | let cmd: string; 154 | if (openDocsInHackage) { 155 | cmd = 'command:haskell.openDocumentationOnHackage?' + encoded; 156 | } else { 157 | cmd = 'command:haskell.showDocumentation?' + encoded; 158 | } 159 | return `[${title}](${cmd})`; 160 | } else if (title === 'Source' && typeof fileAndAnchor === 'string') { 161 | const moduleLocation = fileAndAnchor.replace(/-/gi, '.'); 162 | hackageUri = `https://hackage.haskell.org/package/${packageName}/docs/src/${moduleLocation}`; 163 | const encoded = encodeURIComponent(JSON.stringify({ title, localPath, hackageUri })); 164 | let cmd: string; 165 | if (openSourceInHackage) { 166 | cmd = 'command:haskell.openDocumentationOnHackage?' + encoded; 167 | } else { 168 | cmd = 'command:haskell.showDocumentation?' + encoded; 169 | } 170 | return `[${title}](${cmd})`; 171 | } else { 172 | return s; 173 | } 174 | }, 175 | ); 176 | } 177 | if (typeof ms === 'string') { 178 | return transform(ms); 179 | } else if (ms instanceof MarkdownString) { 180 | const mstr = new MarkdownString(transform(ms.value)); 181 | mstr.isTrusted = true; 182 | return mstr; 183 | } else { 184 | return ms.value; 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/errors.ts: -------------------------------------------------------------------------------- 1 | import { Uri } from 'vscode'; 2 | 3 | export class HlsError extends Error {} 4 | 5 | export class MissingToolError extends HlsError { 6 | public readonly tool: string; 7 | constructor(tool: string) { 8 | let prettyTool: string; 9 | switch (tool.toLowerCase()) { 10 | case 'stack': 11 | prettyTool = 'Stack'; 12 | break; 13 | case 'cabal': 14 | prettyTool = 'Cabal'; 15 | break; 16 | case 'ghc': 17 | prettyTool = 'GHC'; 18 | break; 19 | case 'ghcup': 20 | prettyTool = 'GHCup'; 21 | break; 22 | case 'haskell-language-server': 23 | case 'hls': 24 | prettyTool = 'HLS'; 25 | break; 26 | default: 27 | prettyTool = tool; 28 | break; 29 | } 30 | super(`Project requires ${prettyTool} but it isn't installed`); 31 | this.tool = prettyTool; 32 | } 33 | 34 | public installLink(): Uri | null { 35 | switch (this.tool) { 36 | case 'Stack': 37 | return Uri.parse('https://docs.haskellstack.org/en/stable/install_and_upgrade/'); 38 | case 'GHCup': 39 | case 'Cabal': 40 | case 'HLS': 41 | case 'GHC': 42 | return Uri.parse('https://www.haskell.org/ghcup/'); 43 | default: 44 | return null; 45 | } 46 | } 47 | } 48 | 49 | export class NoMatchingHls extends Error { 50 | constructor(readonly ghcProjVersion: string) { 51 | super(`HLS does not support GHC ${ghcProjVersion} yet.`); 52 | } 53 | public docLink(): Uri { 54 | return Uri.parse('https://haskell-language-server.readthedocs.io/en/latest/support/ghc-version-support.html'); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import { commands, env, ExtensionContext, TextDocument, Uri, window, workspace, WorkspaceFolder } from 'vscode'; 2 | import { 3 | ExecutableOptions, 4 | LanguageClient, 5 | LanguageClientOptions, 6 | Logger, 7 | RevealOutputChannelOn, 8 | ServerOptions, 9 | } from 'vscode-languageclient/node'; 10 | import * as constants from './commands/constants'; 11 | import * as DocsBrowser from './docsBrowser'; 12 | import { HlsError, MissingToolError, NoMatchingHls } from './errors'; 13 | import { findHaskellLanguageServer, HlsExecutable, IEnvVars, fetchConfig } from './hlsBinaries'; 14 | import { addPathToProcessPath, comparePVP, callAsync } from './utils'; 15 | import { Config, initConfig, initLoggerFromConfig, logConfig } from './config'; 16 | import { HaskellStatusBar } from './statusBar'; 17 | 18 | /** 19 | * Global information about the running clients. 20 | */ 21 | type Client = { 22 | client: LanguageClient; 23 | config: Config; 24 | }; 25 | 26 | // The current map of documents & folders to language servers. 27 | // It may be null to indicate that we are in the process of launching a server, 28 | // in which case don't try to launch another one for that uri 29 | const clients: Map = new Map(); 30 | 31 | // This is the entrypoint to our extension 32 | export async function activate(context: ExtensionContext) { 33 | const statusBar = new HaskellStatusBar(context.extension.packageJSON.version as string | undefined); 34 | context.subscriptions.push(statusBar); 35 | 36 | // (Possibly) launch the language server every time a document is opened, so 37 | // it works across multiple workspace folders. Eventually, haskell-lsp should 38 | // just support 39 | // https://microsoft.github.io/language-server-protocol/specifications/specification-3-15/#workspace_workspaceFolders 40 | // and then we can just launch one server 41 | workspace.onDidOpenTextDocument(async (document: TextDocument) => await activateServer(context, document)); 42 | for (const document of workspace.textDocuments) { 43 | await activateServer(context, document); 44 | } 45 | 46 | // Stop the server from any workspace folders that are removed. 47 | workspace.onDidChangeWorkspaceFolders(async (event) => { 48 | for (const folder of event.removed) { 49 | const client = clients.get(folder.uri.toString()); 50 | if (client) { 51 | const uri = folder.uri.toString(); 52 | client.client.info(`Deleting folder for clients: ${uri}`); 53 | clients.delete(uri); 54 | client.client.info('Stopping the server'); 55 | await client.client.stop(); 56 | } 57 | } 58 | }); 59 | 60 | // Register editor commands for HIE, but only register the commands once at activation. 61 | const restartCmd = commands.registerCommand(constants.RestartServerCommandName, async () => { 62 | for (const langClient of clients.values()) { 63 | langClient?.client.info('Stopping the server'); 64 | await langClient?.client.stop(); 65 | langClient?.client.info('Starting the server'); 66 | await langClient?.client.start(); 67 | } 68 | }); 69 | 70 | context.subscriptions.push(restartCmd); 71 | 72 | const openLogsCmd = commands.registerCommand(constants.OpenLogsCommandName, () => { 73 | for (const langClient of clients.values()) { 74 | langClient?.config.outputChannel.show(); 75 | } 76 | }); 77 | 78 | context.subscriptions.push(openLogsCmd); 79 | 80 | const restartExtensionCmd = commands.registerCommand(constants.RestartExtensionCommandName, async () => { 81 | for (const langClient of clients.values()) { 82 | langClient?.client.info('Stopping the server'); 83 | await langClient?.client.stop(); 84 | } 85 | clients.clear(); 86 | fetchConfig(); 87 | 88 | for (const document of workspace.textDocuments) { 89 | await activateServer(context, document); 90 | } 91 | }); 92 | 93 | context.subscriptions.push(restartExtensionCmd); 94 | 95 | const showVersionsCmd = commands.registerCommand(constants.ShowExtensionVersions, () => { 96 | void window.showInformationMessage(`Extension Version: ${context.extension.packageJSON.version ?? ''}`); 97 | }); 98 | 99 | context.subscriptions.push(showVersionsCmd); 100 | 101 | const stopCmd = commands.registerCommand(constants.StopServerCommandName, async () => { 102 | for (const langClient of clients.values()) { 103 | langClient?.client.info('Stopping the server'); 104 | await langClient?.client.stop(); 105 | langClient?.client.info('Server stopped'); 106 | } 107 | }); 108 | 109 | context.subscriptions.push(stopCmd); 110 | 111 | const startCmd = commands.registerCommand(constants.StartServerCommandName, async () => { 112 | for (const langClient of clients.values()) { 113 | langClient?.client.info('Starting the server'); 114 | await langClient?.client.start(); 115 | langClient?.client.info('Server started'); 116 | } 117 | }); 118 | 119 | context.subscriptions.push(startCmd); 120 | 121 | // Set up the documentation browser. 122 | const docsDisposable = DocsBrowser.registerDocsBrowser(); 123 | context.subscriptions.push(docsDisposable); 124 | 125 | const openOnHackageDisposable = DocsBrowser.registerDocsOpenOnHackage(); 126 | context.subscriptions.push(openOnHackageDisposable); 127 | 128 | statusBar.refresh(); 129 | statusBar.show(); 130 | } 131 | 132 | async function activateServer(context: ExtensionContext, document: TextDocument) { 133 | // We are only interested in Haskell files. 134 | if ( 135 | (document.languageId !== 'haskell' && 136 | document.languageId !== 'cabal' && 137 | document.languageId !== 'literate haskell') || 138 | (document.uri.scheme !== 'file' && document.uri.scheme !== 'untitled') 139 | ) { 140 | return; 141 | } 142 | 143 | const uri = document.uri; 144 | const folder = workspace.getWorkspaceFolder(uri); 145 | 146 | await activateServerForFolder(context, uri, folder); 147 | } 148 | 149 | async function activateServerForFolder(context: ExtensionContext, uri: Uri, folder?: WorkspaceFolder) { 150 | const clientsKey = folder ? folder.uri.toString() : uri.toString(); 151 | // If the client already has an LSP server for this uri/folder, then don't start a new one. 152 | if (clients.has(clientsKey)) { 153 | return; 154 | } 155 | // Set the key to null to prevent multiple servers being launched at once 156 | clients.set(clientsKey, null); 157 | 158 | const config = initConfig(workspace.getConfiguration('haskell', uri), uri, folder); 159 | const logger: Logger = initLoggerFromConfig(config); 160 | 161 | logConfig(logger, config); 162 | 163 | let hlsExecutable: HlsExecutable; 164 | try { 165 | hlsExecutable = await findHaskellLanguageServer(context, logger, config.ghcupConfig, config.workingDir, folder); 166 | } catch (e) { 167 | await handleInitializationError(e, logger); 168 | // Make sure to release the key again. 169 | clients.delete(clientsKey); 170 | return; 171 | } 172 | 173 | const serverEnvironment: IEnvVars = initServerEnvironment(config, hlsExecutable); 174 | const exeOptions: ExecutableOptions = { 175 | cwd: config.workingDir, 176 | env: { ...process.env, ...serverEnvironment }, 177 | }; 178 | 179 | // For our intents and purposes, the server should be launched the same way in 180 | // both debug and run mode. 181 | const serverOptions: ServerOptions = { 182 | run: { command: hlsExecutable.location, args: config.serverArgs, options: exeOptions }, 183 | debug: { command: hlsExecutable.location, args: config.serverArgs, options: exeOptions }, 184 | }; 185 | 186 | // If we're operating on a standalone file (i.e. not in a folder) then we need 187 | // to launch the server in a reasonable current directory. Otherwise the cradle 188 | // guessing logic in hie-bios will be wrong! 189 | let cwdMsg = `Activating the language server in working dir: ${config.workingDir}`; 190 | if (folder) { 191 | cwdMsg += ' (the workspace folder)'; 192 | } else { 193 | cwdMsg += ` (parent dir of loaded file ${uri.fsPath})`; 194 | } 195 | logger.info(cwdMsg); 196 | 197 | logger.info(`run command: ${hlsExecutable.location} ${config.serverArgs.join(' ')}`); 198 | logger.info(`debug command: ${hlsExecutable.location} ${config.serverArgs.join(' ')}`); 199 | if (exeOptions.cwd) { 200 | logger.info(`server cwd: ${exeOptions.cwd}`); 201 | } 202 | if (serverEnvironment) { 203 | logger.info('server environment variables:'); 204 | Object.entries(serverEnvironment).forEach(([key, val]: [string, string | undefined]) => { 205 | logger.info(` ${key}=${val}`); 206 | }); 207 | } 208 | 209 | const pat = folder ? `${folder.uri.fsPath}/**/*` : '**/*'; 210 | logger.log(`document selector patten: ${pat}`); 211 | 212 | const cabalDocumentSelector = { scheme: 'file', language: 'cabal', pattern: pat }; 213 | const haskellDocumentSelector = [ 214 | { scheme: 'file', language: 'haskell', pattern: pat }, 215 | { scheme: 'file', language: 'literate haskell', pattern: pat }, 216 | ]; 217 | 218 | const documentSelector = [...haskellDocumentSelector]; 219 | 220 | const cabalFileSupport: 'automatic' | 'enable' | 'disable' = workspace.getConfiguration( 221 | 'haskell', 222 | uri, 223 | ).supportCabalFiles; 224 | logger.info(`Support for '.cabal' files: ${cabalFileSupport}`); 225 | 226 | switch (cabalFileSupport) { 227 | case 'automatic': { 228 | const hlsVersion = await callAsync( 229 | hlsExecutable.location, 230 | ['--numeric-version'], 231 | logger, 232 | config.workingDir, 233 | undefined /* this command is very fast, don't show anything */, 234 | false, 235 | serverEnvironment, 236 | ); 237 | if (comparePVP(hlsVersion, '1.9.0.0') >= 0) { 238 | // If hlsVersion is >= '1.9.0.0' 239 | documentSelector.push(cabalDocumentSelector); 240 | } 241 | break; 242 | } 243 | case 'enable': 244 | documentSelector.push(cabalDocumentSelector); 245 | break; 246 | case 'disable': 247 | break; 248 | default: 249 | break; 250 | } 251 | 252 | const clientOptions: LanguageClientOptions = { 253 | // Use the document selector to only notify the LSP on files inside the folder 254 | // path for the specific workspace. 255 | documentSelector: [...documentSelector], 256 | synchronize: { 257 | // Synchronize the setting section 'haskell' to the server. 258 | configurationSection: 'haskell', 259 | }, 260 | diagnosticCollectionName: config.langName, 261 | revealOutputChannelOn: RevealOutputChannelOn.Never, 262 | outputChannel: config.outputChannel, 263 | outputChannelName: config.langName, 264 | middleware: { 265 | provideHover: DocsBrowser.hoverLinksMiddlewareHook, 266 | provideCompletionItem: DocsBrowser.completionLinksMiddlewareHook, 267 | }, 268 | // Launch the server in the directory of the workspace folder. 269 | workspaceFolder: folder, 270 | }; 271 | 272 | // Create the LSP client. 273 | const langClient = new LanguageClient('haskell', config.langName, serverOptions, clientOptions); 274 | 275 | // Register ClientCapabilities for stuff like window/progress 276 | langClient.registerProposedFeatures(); 277 | 278 | // Finally start the client and add it to the list of clients. 279 | logger.info('Starting language server'); 280 | clients.set(clientsKey, { 281 | client: langClient, 282 | config, 283 | }); 284 | await langClient.start(); 285 | } 286 | 287 | /** 288 | * Handle errors the extension may throw. Errors are expected to be fatal. 289 | * 290 | * @param e Error thrown during the extension initialization. 291 | * @param logger 292 | */ 293 | async function handleInitializationError(e: unknown, logger: Logger) { 294 | if (e instanceof MissingToolError) { 295 | const link = e.installLink(); 296 | if (link) { 297 | if (await window.showErrorMessage(e.message, `Install ${e.tool}`)) { 298 | env.openExternal(link); 299 | } 300 | } else { 301 | await window.showErrorMessage(e.message); 302 | } 303 | } else if (e instanceof HlsError) { 304 | logger.error(`General HlsError: ${e.message}`); 305 | window.showErrorMessage(e.message); 306 | } else if (e instanceof NoMatchingHls) { 307 | const link = e.docLink(); 308 | logger.error(`${e.message}`); 309 | if (await window.showErrorMessage(e.message, 'Open documentation')) { 310 | env.openExternal(link); 311 | } 312 | } else if (e instanceof Error) { 313 | logger.error(`Internal Error: ${e.message}`); 314 | window.showErrorMessage(e.message); 315 | } 316 | if (e instanceof Error) { 317 | // general stack trace printing 318 | if (e.stack) { 319 | logger.error(`${e.stack}`); 320 | } 321 | } 322 | } 323 | 324 | function initServerEnvironment(config: Config, hlsExecutable: HlsExecutable) { 325 | let serverEnvironment: IEnvVars = config.serverEnvironment; 326 | if (hlsExecutable.tag === 'ghcup') { 327 | const newPath = addPathToProcessPath(hlsExecutable.binaryDirectory); 328 | serverEnvironment = { 329 | ...serverEnvironment, 330 | ...{ PATH: newPath }, 331 | }; 332 | } 333 | return serverEnvironment; 334 | } 335 | 336 | /* 337 | * Deactivate each of the LSP servers. 338 | */ 339 | export async function deactivate() { 340 | const promises: Thenable[] = []; 341 | for (const client of clients.values()) { 342 | if (client) { 343 | promises.push(client.client.stop()); 344 | } 345 | } 346 | await Promise.all(promises); 347 | } 348 | -------------------------------------------------------------------------------- /src/ghcup.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as os from 'os'; 3 | import * as process from 'process'; 4 | import { WorkspaceFolder } from 'vscode'; 5 | import { Logger } from 'vscode-languageclient'; 6 | import { MissingToolError } from './errors'; 7 | import { resolvePathPlaceHolders, executableExists, callAsync, ProcessCallback, IEnvVars } from './utils'; 8 | import { match } from 'ts-pattern'; 9 | 10 | export type Tool = 'hls' | 'ghc' | 'cabal' | 'stack'; 11 | 12 | export type ToolConfig = Map; 13 | 14 | export function initDefaultGHCup(config: GHCupConfig, logger: Logger, folder?: WorkspaceFolder): GHCup { 15 | const ghcupLoc = findGHCup(logger, config.executablePath, folder); 16 | return new GHCup(logger, ghcupLoc, config, { 17 | // omit colourful output because the logs are uglier 18 | NO_COLOR: '1', 19 | }); 20 | } 21 | 22 | export type GHCupConfig = { 23 | metadataUrl?: string; 24 | upgradeGHCup: boolean; 25 | executablePath?: string; 26 | }; 27 | 28 | export type ToolInfo = { 29 | tool: Tool; 30 | version: string; 31 | tags: string[]; 32 | }; 33 | 34 | export class GHCup { 35 | constructor( 36 | readonly logger: Logger, 37 | readonly location: string, 38 | readonly config: GHCupConfig, 39 | readonly environment: IEnvVars, 40 | ) {} 41 | 42 | /** 43 | * Most generic way to run the `ghcup` binary. 44 | * @param args Arguments to run the `ghcup` binary with. 45 | * @param title Displayed to the user for long-running tasks. 46 | * @param cancellable Whether this invocation can be cancelled by the user. 47 | * @param callback Handle success or failures. 48 | * @returns The output of the `ghcup` invocation. If no {@link callback} is given, this is the stdout. Otherwise, whatever {@link callback} produces. 49 | */ 50 | public async call( 51 | args: string[], 52 | title?: string, 53 | cancellable?: boolean, 54 | callback?: ProcessCallback, 55 | ): Promise { 56 | const metadataUrl = this.config.metadataUrl; // ; 57 | return await callAsync( 58 | this.location, 59 | ['--no-verbose'].concat(metadataUrl ? ['-s', metadataUrl] : []).concat(args), 60 | this.logger, 61 | undefined, 62 | title, 63 | cancellable, 64 | this.environment, 65 | callback, 66 | ); 67 | } 68 | 69 | /** 70 | * Upgrade the `ghcup` binary unless this option was disabled by the user. 71 | */ 72 | public async upgrade(): Promise { 73 | const upgrade = this.config.upgradeGHCup; 74 | if (upgrade) { 75 | await this.call(['upgrade'], 'Upgrading ghcup', true); 76 | } 77 | } 78 | 79 | /** 80 | * Find the `set` version of a {@link Tool} in GHCup. 81 | * If no version is set, return null. 82 | * @param tool Tool you want to know the latest version of. 83 | * @returns The latest installed or generally available version of the {@link tool} 84 | */ 85 | public async getSetVersion(tool: Tool): Promise { 86 | // these might be custom/stray/compiled, so we try first 87 | const installedVersions = await this.listTool(tool, 'set'); 88 | const latestInstalled = installedVersions.pop(); 89 | if (latestInstalled) { 90 | return latestInstalled; 91 | } else { 92 | return null; 93 | } 94 | } 95 | 96 | /** 97 | * Find the latest version of a {@link Tool} that we can find in GHCup. 98 | * Prefer already installed versions, but fall back to all available versions, if there aren't any. 99 | * @param tool Tool you want to know the latest version of. 100 | * @returns The latest installed or generally available version of the {@link tool} 101 | */ 102 | public async getAnyLatestVersion(tool: Tool): Promise { 103 | // these might be custom/stray/compiled, so we try first 104 | const installedVersions = await this.listTool(tool, 'installed'); 105 | const latestInstalled = installedVersions.pop(); 106 | if (latestInstalled) { 107 | return latestInstalled; 108 | } else { 109 | return this.getLatestAvailableVersion(tool); 110 | } 111 | } 112 | 113 | /** 114 | * Find the latest available version that we can find in GHCup with a certain {@link tag}. 115 | * Corresponds to the `ghcup list -t -c available -r` command. 116 | * The tag can be used to further filter the list of versions, for example you can provide 117 | * @param tool Tool you want to know the latest version of. 118 | * @param tag The tag to filter the available versions with. By default `"latest"`. 119 | * @returns The latest available version filtered by {@link tag}. 120 | */ 121 | public async getLatestAvailableVersion(tool: Tool, tag: string = 'latest'): Promise { 122 | // fall back to installable versions 123 | const availableVersions = await this.listTool(tool, 'available'); 124 | 125 | let latestAvailable: ToolInfo | null = null; 126 | availableVersions.forEach((toolInfo) => { 127 | if (toolInfo.tags.includes(tag)) { 128 | latestAvailable = toolInfo; 129 | } 130 | }); 131 | if (!latestAvailable) { 132 | throw new Error(`Unable to find ${tag} tool ${tool}`); 133 | } else { 134 | return latestAvailable; 135 | } 136 | } 137 | 138 | private async listTool(tool: Tool, category: string): Promise { 139 | // fall back to installable versions 140 | const availableVersions = await this.call(['list', '-t', tool, '-c', category, '-r'], undefined, false).then((s) => 141 | s.split(/\r?\n/), 142 | ); 143 | 144 | return availableVersions.map((toolString) => { 145 | const toolParts = toolString.split(/\s+/); 146 | return { 147 | tool: tool, 148 | version: toolParts[1], 149 | tags: toolParts[2]?.split(',') ?? [], 150 | }; 151 | }); 152 | } 153 | 154 | public async findLatestUserInstalledTool(tool: Tool): Promise { 155 | let toolInfo = null; 156 | toolInfo = await this.getSetVersion(tool); 157 | if (toolInfo) return toolInfo; 158 | toolInfo = await this.getAnyLatestVersion(tool); 159 | if (toolInfo) return toolInfo; 160 | throw new Error(`Unable to find a version for tool ${tool}`); 161 | } 162 | } 163 | 164 | function findGHCup(logger: Logger, exePath?: string, folder?: WorkspaceFolder): string { 165 | logger.info('Checking for ghcup installation'); 166 | if (exePath) { 167 | logger.info(`Trying to find the ghcup executable in: ${exePath}`); 168 | exePath = resolvePathPlaceHolders(exePath, folder); 169 | logger.log(`Location after path variables substitution: ${exePath}`); 170 | if (executableExists(exePath)) { 171 | return exePath; 172 | } else { 173 | throw new Error(`Could not find a ghcup binary at ${exePath}!`); 174 | } 175 | } else { 176 | const localGHCup = ['ghcup'].find(executableExists); 177 | if (!localGHCup) { 178 | logger.info(`probing for GHCup binary`); 179 | const ghcupExe: string | null = match(process.platform) 180 | .with('win32', () => { 181 | const ghcupPrefix = process.env.GHCUP_INSTALL_BASE_PREFIX; 182 | if (ghcupPrefix) { 183 | return path.join(ghcupPrefix, 'ghcup', 'bin', 'ghcup.exe'); 184 | } else { 185 | return path.join('C:\\', 'ghcup', 'bin', 'ghcup.exe'); 186 | } 187 | }) 188 | .otherwise(() => { 189 | const useXDG = process.env.GHCUP_USE_XDG_DIRS; 190 | if (useXDG) { 191 | const xdgBin = process.env.XDG_BIN_HOME; 192 | if (xdgBin) { 193 | return path.join(xdgBin, 'ghcup'); 194 | } else { 195 | return path.join(os.homedir(), '.local', 'bin', 'ghcup'); 196 | } 197 | } else { 198 | const ghcupPrefix = process.env.GHCUP_INSTALL_BASE_PREFIX; 199 | if (ghcupPrefix) { 200 | return path.join(ghcupPrefix, '.ghcup', 'bin', 'ghcup'); 201 | } else { 202 | return path.join(os.homedir(), '.ghcup', 'bin', 'ghcup'); 203 | } 204 | } 205 | }); 206 | if (ghcupExe !== null && executableExists(ghcupExe)) { 207 | return ghcupExe; 208 | } else { 209 | logger.warn(`ghcup at ${ghcupExe} does not exist`); 210 | throw new MissingToolError('ghcup'); 211 | } 212 | } else { 213 | logger.info(`found ghcup at ${localGHCup}`); 214 | return localGHCup; 215 | } 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/hlsBinaries.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import { ConfigurationTarget, ExtensionContext, window, workspace, WorkspaceFolder } from 'vscode'; 4 | import { Logger } from 'vscode-languageclient'; 5 | import { HlsError, MissingToolError, NoMatchingHls } from './errors'; 6 | import { 7 | addPathToProcessPath, 8 | callAsync, 9 | comparePVP, 10 | executableExists, 11 | IEnvVars, 12 | resolvePathPlaceHolders, 13 | } from './utils'; 14 | import { ToolConfig, Tool, initDefaultGHCup, GHCup, GHCupConfig } from './ghcup'; 15 | import { getHlsMetadata } from './metadata'; 16 | export { IEnvVars, fetchConfig }; 17 | 18 | export type Context = { 19 | manageHls: ManageHLS; 20 | storagePath: string; 21 | serverExecutable?: HlsExecutable; 22 | logger: Logger; 23 | }; 24 | 25 | /** 26 | * Global configuration for this extension. 27 | */ 28 | let haskellConfig = workspace.getConfiguration('haskell'); 29 | 30 | /** 31 | * On Windows the executable needs to be stored somewhere with an .exe extension 32 | */ 33 | const exeExt = process.platform === 'win32' ? '.exe' : ''; 34 | 35 | type ManageHLS = 'GHCup' | 'PATH'; 36 | let manageHLS = haskellConfig.get('manageHLS') as ManageHLS; 37 | 38 | function fetchConfig() { 39 | haskellConfig = workspace.getConfiguration('haskell'); 40 | manageHLS = haskellConfig.get('manageHLS') as ManageHLS; 41 | } 42 | 43 | /** 44 | * Gets serverExecutablePath and fails if it's not set. 45 | * @param logger Log progress. 46 | * @param folder Workspace folder. Used for resolving variables in the `serverExecutablePath`. 47 | * @returns Path to an HLS executable binary. 48 | */ 49 | function findServerExecutable(logger: Logger, folder?: WorkspaceFolder): string { 50 | const rawExePath = haskellConfig.get('serverExecutablePath') as string; 51 | logger.info(`Trying to find the server executable in: ${rawExePath}`); 52 | const resolvedExePath = resolvePathPlaceHolders(rawExePath, folder); 53 | logger.log(`Location after path variables substitution: ${resolvedExePath}`); 54 | if (executableExists(resolvedExePath)) { 55 | return resolvedExePath; 56 | } else { 57 | const msg = `Could not find a HLS binary at ${resolvedExePath}! Consider installing HLS via ghcup or change "haskell.manageHLS" in your settings.`; 58 | throw new HlsError(msg); 59 | } 60 | } 61 | 62 | /** 63 | * Searches the `PATH` for `haskell-language-server` or `haskell-language-server-wrapper` binary. 64 | * Fails if nothing is found. 65 | * @param logger Log all the stuff! 66 | * @returns Location of the `haskell-language-server` or `haskell-language-server-wrapper` binary if found. 67 | */ 68 | function findHlsInPath(logger: Logger): string { 69 | // try PATH 70 | const exes: string[] = ['haskell-language-server-wrapper', 'haskell-language-server']; 71 | logger.info(`Searching for server executables ${exes.join(',')} in $PATH`); 72 | logger.info(`$PATH environment variable: ${process.env.PATH}`); 73 | for (const exe of exes) { 74 | if (executableExists(exe)) { 75 | logger.info(`Found server executable in $PATH: ${exe}`); 76 | return exe; 77 | } 78 | } 79 | throw new MissingToolError('hls'); 80 | } 81 | 82 | export type HlsExecutable = HlsOnPath | HlsViaVSCodeConfig | HlsViaGhcup; 83 | 84 | export type HlsOnPath = { 85 | location: string; 86 | tag: 'path'; 87 | }; 88 | 89 | export type HlsViaVSCodeConfig = { 90 | location: string; 91 | tag: 'config'; 92 | }; 93 | 94 | export type HlsViaGhcup = { 95 | location: string; 96 | /** 97 | * if we download HLS, add that bin dir to PATH 98 | */ 99 | binaryDirectory: string; 100 | tag: 'ghcup'; 101 | }; 102 | 103 | /** 104 | * Find and setup the Haskell Language Server. 105 | * 106 | * We support three ways of finding the HLS binary: 107 | * 108 | * 1. Let the user provide a location via `haskell.serverExecutablePath` option. 109 | * 2. Find a `haskell-language-server` binary on the `$PATH` if the user wants to do that. 110 | * 3. Use GHCup to install and locate HLS and other required tools, such as cabal, stack and ghc. 111 | * 112 | * @param context Context of the extension, required for metadata. 113 | * @param logger Logger for progress updates. 114 | * @param workingDir Working directory in VSCode. 115 | * @param folder Optional workspace folder. If given, will be preferred over {@link workingDir} for finding configuration entries. 116 | * @returns Path to haskell-language-server, paired with additional data required for setting up. 117 | */ 118 | export async function findHaskellLanguageServer( 119 | context: ExtensionContext, 120 | logger: Logger, 121 | ghcupConfig: GHCupConfig, 122 | workingDir: string, 123 | folder?: WorkspaceFolder, 124 | ): Promise { 125 | logger.info('Finding haskell-language-server'); 126 | 127 | const hasConfigForExecutable = haskellConfig.get('serverExecutablePath') as string; 128 | if (hasConfigForExecutable) { 129 | const exe = findServerExecutable(logger, folder); 130 | return { 131 | location: exe, 132 | tag: 'config', 133 | }; 134 | } 135 | 136 | const storagePath: string = getStoragePath(context); 137 | if (!fs.existsSync(storagePath)) { 138 | fs.mkdirSync(storagePath); 139 | } 140 | 141 | // first extension initialization 142 | manageHLS = await promptUserForManagingHls(context, manageHLS); 143 | 144 | // based on the user-decision 145 | if (manageHLS === 'PATH') { 146 | const exe = findHlsInPath(logger); 147 | return { 148 | location: exe, 149 | tag: 'path', 150 | }; 151 | } else { 152 | // we manage HLS, make sure ghcup is installed/available 153 | const ghcup = initDefaultGHCup(ghcupConfig, logger, folder); 154 | await ghcup.upgrade(); 155 | 156 | // boring init 157 | let latestHLS: string | undefined | null; 158 | let latestCabal: string | undefined | null; 159 | let latestStack: string | undefined | null; 160 | let recGHC: string | undefined | null = 'recommended'; 161 | let projectHls: string | undefined | null; 162 | let projectGhc: string | undefined | null; 163 | 164 | // support explicit toolchain config 165 | const toolchainConfig = new Map(Object.entries(haskellConfig.get('toolchain') as ToolConfig)) as ToolConfig; 166 | if (toolchainConfig) { 167 | latestHLS = toolchainConfig.get('hls'); 168 | latestCabal = toolchainConfig.get('cabal'); 169 | latestStack = toolchainConfig.get('stack'); 170 | recGHC = toolchainConfig.get('ghc'); 171 | 172 | projectHls = latestHLS; 173 | projectGhc = recGHC; 174 | } 175 | 176 | // get a preliminary toolchain for finding the correct project GHC version 177 | // (we need HLS and cabal/stack and ghc as fallback), 178 | // later we may install a different toolchain that's more project-specific 179 | if (latestHLS === undefined) { 180 | latestHLS = await ghcup.getAnyLatestVersion('hls').then((tool) => tool?.version); 181 | } 182 | if (latestCabal === undefined) { 183 | latestCabal = (await ghcup.findLatestUserInstalledTool('cabal')).version; 184 | } 185 | if (latestStack === undefined) { 186 | latestStack = (await ghcup.findLatestUserInstalledTool('stack')).version; 187 | } 188 | if (recGHC === undefined) { 189 | recGHC = !executableExists('ghc') ? (await ghcup.getLatestAvailableVersion('ghc', 'recommended')).version : null; 190 | } 191 | 192 | // download popups 193 | const promptBeforeDownloads = haskellConfig.get('promptBeforeDownloads') as boolean; 194 | if (promptBeforeDownloads) { 195 | const hlsInstalled = latestHLS ? await installationStatusOfGhcupTool(ghcup, 'hls', latestHLS) : undefined; 196 | const cabalInstalled = latestCabal ? await installationStatusOfGhcupTool(ghcup, 'cabal', latestCabal) : undefined; 197 | const stackInstalled = latestStack ? await installationStatusOfGhcupTool(ghcup, 'stack', latestStack) : undefined; 198 | const ghcInstalled = executableExists('ghc') 199 | ? new ToolStatus( 200 | 'ghc', 201 | await callAsync(`ghc${exeExt}`, ['--numeric-version'], logger, undefined, undefined, false), 202 | ) 203 | : // if recGHC is null, that means user disabled automatic handling, 204 | recGHC !== null 205 | ? await installationStatusOfGhcupTool(ghcup, 'ghc', recGHC) 206 | : undefined; 207 | const toInstall: ToolStatus[] = [hlsInstalled, cabalInstalled, stackInstalled, ghcInstalled].filter( 208 | (tool) => tool && !tool.installed, 209 | ) as ToolStatus[]; 210 | if (toInstall.length > 0) { 211 | const decision = await window.showInformationMessage( 212 | `Need to download ${toInstall.map((t) => t.nameWithVersion).join(', ')}, continue?`, 213 | 'Yes', 214 | 'No', 215 | "Yes, don't ask again", 216 | ); 217 | if (decision === 'Yes') { 218 | logger.info(`User accepted download for ${toInstall.map((t) => t.nameWithVersion).join(', ')}.`); 219 | } else if (decision === "Yes, don't ask again") { 220 | logger.info( 221 | `User accepted download for ${toInstall.map((t) => t.nameWithVersion).join(', ')} and won't be asked again.`, 222 | ); 223 | haskellConfig.update('promptBeforeDownloads', false); 224 | } else { 225 | toInstall.forEach((tool) => { 226 | if (tool !== undefined && !tool.installed) { 227 | if (tool.name === 'hls') { 228 | throw new MissingToolError('hls'); 229 | } else if (tool.name === 'cabal') { 230 | latestCabal = null; 231 | } else if (tool.name === 'stack') { 232 | latestStack = null; 233 | } else if (tool.name === 'ghc') { 234 | recGHC = null; 235 | } 236 | } 237 | }); 238 | } 239 | } 240 | } 241 | 242 | // our preliminary toolchain 243 | const latestToolchainBindir = await ghcup.call( 244 | [ 245 | 'run', 246 | ...(latestHLS ? ['--hls', latestHLS] : []), 247 | ...(latestCabal ? ['--cabal', latestCabal] : []), 248 | ...(latestStack ? ['--stack', latestStack] : []), 249 | ...(recGHC ? ['--ghc', recGHC] : []), 250 | '--install', 251 | ], 252 | 'Installing latest toolchain for bootstrap', 253 | true, 254 | (err, stdout, _stderr, resolve, reject) => { 255 | if (err) { 256 | reject("Couldn't install latest toolchain"); 257 | } else { 258 | resolve(stdout?.trim()); 259 | } 260 | }, 261 | ); 262 | 263 | // now figure out the actual project GHC version and the latest supported HLS version 264 | // we need for it (e.g. this might in fact be a downgrade for old GHCs) 265 | if (projectHls === undefined || projectGhc === undefined) { 266 | const res = await getLatestProjectHls(ghcup, logger, storagePath, workingDir, latestToolchainBindir); 267 | if (projectHls === undefined) { 268 | projectHls = res[0]; 269 | } 270 | if (projectGhc === undefined) { 271 | projectGhc = res[1]; 272 | } 273 | } 274 | 275 | // more download popups 276 | if (promptBeforeDownloads) { 277 | const hlsInstalled = projectHls ? await installationStatusOfGhcupTool(ghcup, 'hls', projectHls) : undefined; 278 | const ghcInstalled = projectGhc ? await installationStatusOfGhcupTool(ghcup, 'ghc', projectGhc) : undefined; 279 | const toInstall: ToolStatus[] = [hlsInstalled, ghcInstalled].filter( 280 | (tool) => tool && !tool.installed, 281 | ) as ToolStatus[]; 282 | if (toInstall.length > 0) { 283 | const decision = await window.showInformationMessage( 284 | `Need to download ${toInstall.map((t) => t.nameWithVersion).join(', ')}, continue?`, 285 | { modal: true }, 286 | 'Yes', 287 | 'No', 288 | "Yes, don't ask again", 289 | ); 290 | if (decision === 'Yes') { 291 | logger.info(`User accepted download for ${toInstall.map((t) => t.nameWithVersion).join(', ')}.`); 292 | } else if (decision === "Yes, don't ask again") { 293 | logger.info( 294 | `User accepted download for ${toInstall.map((t) => t.nameWithVersion).join(', ')} and won't be asked again.`, 295 | ); 296 | haskellConfig.update('promptBeforeDownloads', false); 297 | } else { 298 | toInstall.forEach((tool) => { 299 | if (!tool.installed) { 300 | if (tool.name === 'hls') { 301 | throw new MissingToolError('hls'); 302 | } else if (tool.name === 'ghc') { 303 | projectGhc = null; 304 | } 305 | } 306 | }); 307 | } 308 | } 309 | } 310 | 311 | // now install the proper versions 312 | const hlsBinDir = await ghcup.call( 313 | [ 314 | 'run', 315 | ...(projectHls ? ['--hls', projectHls] : []), 316 | ...(latestCabal ? ['--cabal', latestCabal] : []), 317 | ...(latestStack ? ['--stack', latestStack] : []), 318 | ...(projectGhc ? ['--ghc', projectGhc] : []), 319 | '--install', 320 | ], 321 | `Installing project specific toolchain: ${[ 322 | ['hls', projectHls], 323 | ['GHC', projectGhc], 324 | ['cabal', latestCabal], 325 | ['stack', latestStack], 326 | ] 327 | .filter((t) => t[1]) 328 | .map((t) => `${t[0]}-${t[1]}`) 329 | .join(', ')}`, 330 | true, 331 | ); 332 | 333 | if (projectHls) { 334 | return { 335 | binaryDirectory: hlsBinDir, 336 | location: path.join(hlsBinDir, `haskell-language-server-wrapper${exeExt}`), 337 | tag: 'ghcup', 338 | }; 339 | } else { 340 | return { 341 | binaryDirectory: hlsBinDir, 342 | location: findHlsInPath(logger), 343 | tag: 'ghcup', 344 | }; 345 | } 346 | } 347 | } 348 | 349 | async function promptUserForManagingHls(context: ExtensionContext, manageHlsSetting: ManageHLS): Promise { 350 | if (manageHlsSetting !== 'GHCup' && (!context.globalState.get('pluginInitialized') as boolean | null)) { 351 | const promptMessage = `How do you want the extension to manage/discover HLS and the relevant toolchain? 352 | 353 | Choose "Automatically" if you're in doubt. 354 | `; 355 | 356 | const popup = window.showInformationMessage( 357 | promptMessage, 358 | { modal: true }, 359 | 'Automatically via GHCup', 360 | 'Manually via PATH', 361 | ); 362 | 363 | const decision = (await popup) || null; 364 | let howToManage: ManageHLS; 365 | if (decision === 'Automatically via GHCup') { 366 | howToManage = 'GHCup'; 367 | } else if (decision === 'Manually via PATH') { 368 | howToManage = 'PATH'; 369 | } else { 370 | window.showWarningMessage( 371 | "Choosing default PATH method for HLS discovery. You can change this via 'haskell.manageHLS' in the settings.", 372 | ); 373 | howToManage = 'PATH'; 374 | } 375 | haskellConfig.update('manageHLS', howToManage, ConfigurationTarget.Global); 376 | context.globalState.update('pluginInitialized', true); 377 | return howToManage; 378 | } else { 379 | return manageHlsSetting; 380 | } 381 | } 382 | 383 | async function getLatestProjectHls( 384 | ghcup: GHCup, 385 | logger: Logger, 386 | storagePath: string, 387 | workingDir: string, 388 | toolchainBindir: string, 389 | ): Promise<[string, string]> { 390 | // get project GHC version, but fallback to system ghc if necessary. 391 | const projectGhc = toolchainBindir 392 | ? await getProjectGhcVersion(toolchainBindir, workingDir, logger).catch(async (e) => { 393 | logger.error(`${e}`); 394 | window.showWarningMessage( 395 | `I had trouble figuring out the exact GHC version for the project. Falling back to using 'ghc${exeExt}'.`, 396 | ); 397 | return await callAsync(`ghc${exeExt}`, ['--numeric-version'], logger, undefined, undefined, false); 398 | }) 399 | : await callAsync(`ghc${exeExt}`, ['--numeric-version'], logger, undefined, undefined, false); 400 | 401 | // first we get supported GHC versions from available HLS bindists (whether installed or not) 402 | const metadataMap = (await getHlsMetadata(storagePath, logger)) || new Map(); 403 | // then we get supported GHC versions from currently installed HLS versions 404 | const ghcupMap = (await findAvailableHlsBinariesFromGHCup(ghcup)) || new Map(); 405 | // since installed HLS versions may support a different set of GHC versions than the bindists 406 | // (e.g. because the user ran 'ghcup compile hls'), we need to merge both maps, preferring 407 | // values from already installed HLSes 408 | const merged = new Map([...metadataMap, ...ghcupMap]); // right-biased 409 | // now sort and get the latest suitable version 410 | const latest = [...merged] 411 | .filter(([_k, v]) => v.some((x) => x === projectGhc)) 412 | .sort(([k1, _v1], [k2, _v2]) => comparePVP(k1, k2)) 413 | .pop(); 414 | 415 | if (!latest) { 416 | throw new NoMatchingHls(projectGhc); 417 | } else { 418 | return [latest[0], projectGhc]; 419 | } 420 | } 421 | 422 | /** 423 | * Obtain the project ghc version from the HLS - Wrapper (which must be in PATH now). 424 | * Also, serves as a sanity check. 425 | * @param toolchainBindir Path to the toolchain bin directory (added to PATH) 426 | * @param workingDir Directory to run the process, usually the root of the workspace. 427 | * @param logger Logger for feedback. 428 | * @returns The GHC version, or fail with an `Error`. 429 | */ 430 | export async function getProjectGhcVersion( 431 | toolchainBindir: string, 432 | workingDir: string, 433 | logger: Logger, 434 | ): Promise { 435 | const title = 'Working out the project GHC version. This might take a while...'; 436 | logger.info(title); 437 | 438 | const args = ['--project-ghc-version']; 439 | 440 | const newPath = addPathToProcessPath(toolchainBindir); 441 | const environmentNew: IEnvVars = { 442 | PATH: newPath, 443 | }; 444 | 445 | return callAsync( 446 | 'haskell-language-server-wrapper', 447 | args, 448 | logger, 449 | workingDir, 450 | title, 451 | false, 452 | environmentNew, 453 | (err, stdout, stderr, resolve, reject) => { 454 | if (err) { 455 | // Error message emitted by HLS-wrapper 456 | const regex = 457 | /Cradle requires (.+) but couldn't find it|The program '(.+)' version .* is required but the version of.*could.*not be determined|Cannot find the program '(.+)'\. User-specified/; 458 | const res = regex.exec(stderr); 459 | if (res) { 460 | for (let i = 1; i < res.length; i++) { 461 | if (res[i]) { 462 | reject(new MissingToolError(res[i])); 463 | } 464 | } 465 | reject(new MissingToolError('unknown')); 466 | } 467 | reject( 468 | Error( 469 | `haskell-language-server --project-ghc-version exited with exit code ${err.code}:\n${stdout}\n${stderr}`, 470 | ), 471 | ); 472 | } else { 473 | logger.info(`The GHC version for the project or file: ${stdout?.trim()}`); 474 | resolve(stdout?.trim()); 475 | } 476 | }, 477 | ); 478 | } 479 | 480 | /** 481 | * Find the storage path for the extension. 482 | * If no custom location was given 483 | * 484 | * @param context Extension context for the 'Storage Path'. 485 | * @returns 486 | */ 487 | export function getStoragePath(context: ExtensionContext): string { 488 | let storagePath: string | undefined = haskellConfig.get('releasesDownloadStoragePath'); 489 | 490 | if (!storagePath) { 491 | storagePath = context.globalStorageUri.fsPath; 492 | } else { 493 | storagePath = resolvePathPlaceHolders(storagePath); 494 | } 495 | 496 | return storagePath; 497 | } 498 | 499 | /** 500 | * 501 | * Complements {@link getReleaseMetadata}, by checking possibly locally compiled 502 | * HLS in ghcup 503 | * If 'targetGhc' is omitted, picks the latest 'haskell-language-server-wrapper', 504 | * otherwise ensures the specified GHC is supported. 505 | * 506 | * @param ghcup GHCup wrapper. 507 | * @returns A Map of the locally installed HLS versions and with which `GHC` versions they are compatible. 508 | */ 509 | 510 | async function findAvailableHlsBinariesFromGHCup(ghcup: GHCup): Promise | null> { 511 | const hlsVersions = await ghcup.call(['list', '-t', 'hls', '-c', 'installed', '-r'], undefined, false); 512 | 513 | const bindir = await ghcup.call(['whereis', 'bindir'], undefined, false); 514 | const files = fs.readdirSync(bindir).filter((e) => { 515 | const stat = fs.statSync(path.join(bindir, e)); 516 | return stat.isFile(); 517 | }); 518 | 519 | const installed = hlsVersions.split(/\r?\n/).map((e) => e.split(/\s+/)[1]); 520 | if (installed?.length) { 521 | const myMap = new Map(); 522 | installed.forEach((hls) => { 523 | const ghcs = files 524 | .filter((f) => f.endsWith(`~${hls}${exeExt}`) && f.startsWith('haskell-language-server-')) 525 | .map((f) => { 526 | const rmPrefix = f.substring('haskell-language-server-'.length); 527 | return rmPrefix.substring(0, rmPrefix.length - `~${hls}${exeExt}`.length); 528 | }); 529 | myMap.set(hls, ghcs); 530 | }); 531 | return myMap; 532 | } else { 533 | return null; 534 | } 535 | } 536 | 537 | async function installationStatusOfGhcupTool(ghcup: GHCup, tool: Tool, version: string): Promise { 538 | const b = await ghcup 539 | .call(['whereis', tool, version], undefined, false) 540 | .then(() => true) 541 | .catch(() => false); 542 | return new ToolStatus(tool, version, b); 543 | } 544 | 545 | /** 546 | * Tracks the name, version and installation state of tools we need. 547 | */ 548 | class ToolStatus { 549 | /** 550 | * "\-\" of the installed Tool. 551 | */ 552 | readonly nameWithVersion: string = ''; 553 | 554 | /** 555 | * Initialize an installed tool entry. 556 | * 557 | * If optional parameters are omitted, we assume the tool is installed. 558 | * 559 | * @param name Name of the tool. 560 | * @param version Version of the tool, expected to be either SemVer or PVP versioned. 561 | * @param installed Is this tool currently installed? 562 | */ 563 | public constructor( 564 | readonly name: string, 565 | readonly version: string, 566 | readonly installed: boolean = true, 567 | ) { 568 | this.nameWithVersion = `${name}-${version}`; 569 | } 570 | } 571 | -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | import { OutputChannel } from "vscode"; 2 | import { Logger } from "vscode-languageclient"; 3 | import * as fs from 'fs'; 4 | 5 | enum LogLevel { 6 | Off, 7 | Error, 8 | Warn, 9 | Info, 10 | Debug, 11 | } 12 | export class ExtensionLogger implements Logger { 13 | public readonly name: string; 14 | public readonly level: LogLevel; 15 | public readonly channel: OutputChannel; 16 | public readonly logFile: string | undefined; 17 | 18 | constructor(name: string, level: string, channel: OutputChannel, logFile: string | undefined) { 19 | this.name = name; 20 | this.level = this.getLogLevel(level); 21 | this.channel = channel; 22 | this.logFile = logFile; 23 | } 24 | public warn(message: string): void { 25 | this.logLevel(LogLevel.Warn, message); 26 | } 27 | 28 | public info(message: string): void { 29 | this.logLevel(LogLevel.Info, message); 30 | } 31 | 32 | public error(message: string) { 33 | this.logLevel(LogLevel.Error, message); 34 | } 35 | 36 | public log(message: string) { 37 | this.logLevel(LogLevel.Debug, message); 38 | } 39 | 40 | private write(msg: string) { 41 | let now = new Date(); 42 | // Ugly hack to make js date iso format similar to hls one 43 | const offset = now.getTimezoneOffset(); 44 | now = new Date(now.getTime() - offset * 60 * 1000); 45 | const timedMsg = `${new Date().toISOString().replace('T', ' ').replace('Z', '0000')} ${msg}`; 46 | this.channel.appendLine(timedMsg); 47 | if (this.logFile) { 48 | fs.appendFileSync(this.logFile, timedMsg + '\n'); 49 | } 50 | } 51 | 52 | private logLevel(level: LogLevel, msg: string) { 53 | if (level <= this.level) { 54 | this.write(`[${this.name}] ${LogLevel[level].toUpperCase()} ${msg}`); 55 | } 56 | } 57 | 58 | private getLogLevel(level: string) { 59 | switch (level) { 60 | case 'off': 61 | return LogLevel.Off; 62 | case 'error': 63 | return LogLevel.Error; 64 | case 'debug': 65 | return LogLevel.Debug; 66 | default: 67 | return LogLevel.Info; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/metadata.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as https from 'https'; 3 | import * as path from 'path'; 4 | import { match } from 'ts-pattern'; 5 | import { promisify } from 'util'; 6 | import { window, workspace } from 'vscode'; 7 | import { Logger } from 'vscode-languageclient'; 8 | import { httpsGetSilently } from './utils'; 9 | 10 | /** 11 | * Metadata of release information. 12 | * 13 | * Example of the expected format: 14 | * 15 | * ``` 16 | * { 17 | * "1.6.1.0": { 18 | * "A_64": { 19 | * "Darwin": [ 20 | * "8.10.6", 21 | * ], 22 | * "Linux_Alpine": [ 23 | * "8.10.7", 24 | * "8.8.4", 25 | * ], 26 | * }, 27 | * "A_ARM": { 28 | * "Linux_UnknownLinux": [ 29 | * "8.10.7" 30 | * ] 31 | * }, 32 | * "A_ARM64": { 33 | * "Darwin": [ 34 | * "8.10.7" 35 | * ], 36 | * "Linux_UnknownLinux": [ 37 | * "8.10.7" 38 | * ] 39 | * } 40 | * } 41 | * } 42 | * ``` 43 | * 44 | * consult [ghcup metadata repo](https://github.com/haskell/ghcup-metadata/) for details. 45 | */ 46 | export type ReleaseMetadata = Map>>; 47 | 48 | export type Platform = 'Darwin' | 'Linux_UnknownLinux' | 'Windows' | 'FreeBSD'; 49 | 50 | export type Arch = 'A_ARM' | 'A_ARM64' | 'A_32' | 'A_64'; 51 | 52 | /** 53 | * Compute Map of supported HLS versions for this platform. 54 | * Fetches HLS metadata information. 55 | * 56 | * @param storagePath Path to put in binary files and caches. 57 | * @param logger Logger for feedback 58 | * @returns Map of supported HLS versions or null if metadata could not be fetched. 59 | */ 60 | export async function getHlsMetadata(storagePath: string, logger: Logger): Promise | null> { 61 | const metadata = await getReleaseMetadata(storagePath, logger).catch(() => null); 62 | if (!metadata) { 63 | window.showErrorMessage('Could not get release metadata'); 64 | return null; 65 | } 66 | const plat: Platform | null = match(process.platform) 67 | .with('darwin', () => 'Darwin' as Platform) 68 | .with('linux', () => 'Linux_UnknownLinux' as Platform) 69 | .with('win32', () => 'Windows' as Platform) 70 | .with('freebsd', () => 'FreeBSD' as Platform) 71 | .otherwise(() => null); 72 | if (plat === null) { 73 | throw new Error(`Unknown platform ${process.platform}`); 74 | } 75 | const arch: Arch | null = match(process.arch) 76 | .with('arm', () => 'A_ARM' as Arch) 77 | .with('arm64', () => 'A_ARM64' as Arch) 78 | .with('ia32', () => 'A_32' as Arch) 79 | .with('x64', () => 'A_64' as Arch) 80 | .otherwise(() => null); 81 | if (arch === null) { 82 | throw new Error(`Unknown architecture ${process.arch}`); 83 | } 84 | 85 | return findSupportedHlsPerGhc(plat, arch, metadata, logger); 86 | } 87 | /** 88 | * Find all supported GHC versions per HLS version supported on the given 89 | * platform and architecture. 90 | * @param platform Platform of the host. 91 | * @param arch Arch of the host. 92 | * @param metadata HLS Metadata information. 93 | * @param logger Logger. 94 | * @returns Map from HLS version to GHC versions that are supported. 95 | */ 96 | export function findSupportedHlsPerGhc( 97 | platform: Platform, 98 | arch: Arch, 99 | metadata: ReleaseMetadata, 100 | logger: Logger, 101 | ): Map { 102 | logger.info(`Platform constants: ${platform}, ${arch}`); 103 | const newMap = new Map(); 104 | metadata.forEach((supportedArch, hlsVersion) => { 105 | const supportedOs = supportedArch.get(arch); 106 | if (supportedOs) { 107 | const ghcSupportedOnOs = supportedOs.get(platform); 108 | if (ghcSupportedOnOs) { 109 | logger.log(`HLS ${hlsVersion} compatible with GHC Versions: ${ghcSupportedOnOs.join(',')}`); 110 | // copy supported ghc versions to avoid unintended modifications 111 | newMap.set(hlsVersion, [...ghcSupportedOnOs]); 112 | } 113 | } 114 | }); 115 | 116 | return newMap; 117 | } 118 | 119 | /** 120 | * Download GHCUP metadata. 121 | * 122 | * @param storagePath Path to put in binary files and caches. 123 | * @param logger Logger for feedback. 124 | * @returns Metadata of releases, or null if the cache can not be found. 125 | */ 126 | async function getReleaseMetadata(storagePath: string, logger: Logger): Promise { 127 | const releasesUrl = workspace.getConfiguration('haskell').releasesURL 128 | ? new URL(workspace.getConfiguration('haskell').releasesURL as string) 129 | : undefined; 130 | const opts: https.RequestOptions = releasesUrl 131 | ? { 132 | host: releasesUrl.host, 133 | path: releasesUrl.pathname, 134 | } 135 | : { 136 | host: 'raw.githubusercontent.com', 137 | path: '/haskell/ghcup-metadata/master/hls-metadata-0.0.1.json', 138 | }; 139 | 140 | const offlineCache = path.join(storagePath, 'ghcupReleases.cache.json'); 141 | 142 | /** 143 | * Convert a json value to ReleaseMetadata. 144 | * Assumes the json is well-formed and a valid Release-Metadata. 145 | * @param someObj Release Metadata without any typing information but well-formed. 146 | * @returns Typed ReleaseMetadata. 147 | */ 148 | const objectToMetadata = (someObj: any): ReleaseMetadata => { 149 | const obj = someObj as [string: [string: [string: string[]]]]; 150 | const hlsMetaEntries = Object.entries(obj).map(([hlsVersion, archMap]) => { 151 | const archMetaEntries = Object.entries(archMap).map(([arch, supportedGhcVersionsPerOs]) => { 152 | return [arch, new Map(Object.entries(supportedGhcVersionsPerOs))] as [string, Map]; 153 | }); 154 | return [hlsVersion, new Map(archMetaEntries)] as [string, Map>]; 155 | }); 156 | return new Map(hlsMetaEntries); 157 | }; 158 | 159 | async function readCachedReleaseData(): Promise { 160 | try { 161 | logger.info(`Reading cached release data at ${offlineCache}`); 162 | const cachedInfo = await promisify(fs.readFile)(offlineCache, { encoding: 'utf-8' }); 163 | // export type ReleaseMetadata = Map>>; 164 | const value: any = JSON.parse(cachedInfo); 165 | return objectToMetadata(value); 166 | } catch (err: any) { 167 | // If file doesn't exist, return null, otherwise consider it a failure 168 | if (err.code === 'ENOENT') { 169 | logger.warn(`No cached release data found at ${offlineCache}`); 170 | return null; 171 | } 172 | throw err; 173 | } 174 | } 175 | 176 | try { 177 | const releaseInfo = await httpsGetSilently(opts); 178 | const releaseInfoParsed = JSON.parse(releaseInfo); 179 | 180 | // Cache the latest successfully fetched release information 181 | await promisify(fs.writeFile)(offlineCache, JSON.stringify(releaseInfoParsed), { encoding: 'utf-8' }); 182 | return objectToMetadata(releaseInfoParsed); 183 | } catch (githubError: any) { 184 | // Attempt to read from the latest cached file 185 | try { 186 | const cachedInfoParsed = await readCachedReleaseData(); 187 | 188 | window.showWarningMessage( 189 | "Couldn't get the latest haskell-language-server releases from GitHub, used local cache instead: " + 190 | githubError.message, 191 | ); 192 | return cachedInfoParsed; 193 | } catch (_fileError) { 194 | throw new Error("Couldn't get the latest haskell-language-server releases from GitHub: " + githubError.message); 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/statusBar.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as constants from './commands/constants'; 3 | 4 | export class HaskellStatusBar { 5 | readonly item: vscode.StatusBarItem; 6 | constructor(readonly version?: string) { 7 | // Set up the status bar item. 8 | this.item = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left); 9 | } 10 | 11 | refresh(): void { 12 | const version = this.version ?? ''; 13 | this.item.text = `Haskell`; 14 | 15 | this.item.command = constants.OpenLogsCommandName; 16 | this.item.tooltip = new vscode.MarkdownString('', true); 17 | this.item.tooltip.isTrusted = true; 18 | this.item.tooltip.appendMarkdown( 19 | `[Extension Info](command:${constants.ShowExtensionVersions} "Show Extension Version"): Version ${version}\n\n` + 20 | `---\n\n` + 21 | `[$(terminal) Open Logs](command:${constants.OpenLogsCommandName} "Open the logs of the Server and Extension")\n\n` + 22 | `[$(debug-restart) Restart Server](command:${constants.RestartServerCommandName} "Restart Haskell Language Server")\n\n` + 23 | `[$(refresh) Restart Extension](command:${constants.RestartExtensionCommandName} "Restart vscode-haskell Extension")\n\n`, 24 | ); 25 | } 26 | 27 | show() { 28 | this.item.show(); 29 | } 30 | 31 | hide() { 32 | this.item.hide(); 33 | } 34 | 35 | dispose() { 36 | this.item.dispose(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import * as child_process from 'child_process'; 2 | import * as fs from 'fs'; 3 | import * as https from 'https'; 4 | import * as os from 'os'; 5 | import * as process from 'process'; 6 | import { ProgressLocation, window, workspace, WorkspaceFolder } from 'vscode'; 7 | import { Logger } from 'vscode-languageclient'; 8 | import * as which from 'which'; 9 | import { HlsError } from './errors'; 10 | 11 | // Used for environment variables later on 12 | export type IEnvVars = { 13 | [key: string]: string; 14 | }; 15 | 16 | /** 17 | * Callback invoked on process termination. 18 | */ 19 | export type ProcessCallback = ( 20 | error: child_process.ExecFileException | null, 21 | stdout: string, 22 | stderr: string, 23 | resolve: (value: string | PromiseLike) => void, 24 | reject: (reason?: HlsError | Error | string) => void, 25 | ) => void; 26 | 27 | /** 28 | * Call a process asynchronously. 29 | * While doing so, update the windows with progress information. 30 | * If you need to run a process, consider preferring this over running 31 | * the command directly. 32 | * 33 | * @param binary Name of the binary to invoke. 34 | * @param args Arguments passed directly to the binary. 35 | * @param dir Directory in which the process shall be executed. 36 | * @param logger Logger for progress updates. 37 | * @param title Title of the action, shown to users if available. 38 | * @param cancellable Can the user cancel this process invocation? 39 | * @param envAdd Extra environment variables for this process only. 40 | * @param callback Upon process termination, execute this callback. If given, must resolve promise. On error, stderr and stdout are logged regardless of whether the callback has been specified. 41 | * @returns Stdout of the process invocation, trimmed off newlines, or whatever the `callback` resolved to. 42 | */ 43 | export function callAsync( 44 | binary: string, 45 | args: string[], 46 | logger: Logger, 47 | dir?: string, 48 | title?: string, 49 | cancellable?: boolean, 50 | envAdd?: IEnvVars, 51 | callback?: ProcessCallback, 52 | ): Thenable { 53 | let newEnv: IEnvVars = resolveServerEnvironmentPATH( 54 | workspace.getConfiguration('haskell').get('serverEnvironment') || {}, 55 | ); 56 | newEnv = { ...(process.env as IEnvVars), ...newEnv, ...(envAdd || {}) }; 57 | return window.withProgress( 58 | { 59 | location: ProgressLocation.Notification, 60 | title, 61 | cancellable, 62 | }, 63 | async (_, token) => { 64 | return new Promise((resolve, reject) => { 65 | const command: string = binary + ' ' + args.join(' '); 66 | logger.info(`Executing '${command}' in cwd '${dir ? dir : process.cwd()}'`); 67 | token.onCancellationRequested(() => { 68 | logger.warn(`User canceled the execution of '${command}'`); 69 | }); 70 | // Need to set the encoding to 'utf8' in order to get back a string 71 | // We execute the command in a shell for windows, to allow use .cmd or .bat scripts 72 | const childProcess = child_process 73 | .execFile( 74 | process.platform === 'win32' ? `"${binary}"` : binary, 75 | args, 76 | { encoding: 'utf8', cwd: dir, shell: process.platform === 'win32', env: newEnv }, 77 | (err, stdout, stderr) => { 78 | if (err) { 79 | logger.error(`Error executing '${command}' with error code ${err.code}`); 80 | logger.error(`stderr: ${stderr}`); 81 | if (stdout) { 82 | logger.error(`stdout: ${stdout}`); 83 | } 84 | } 85 | if (callback) { 86 | callback(err, stdout, stderr, resolve, reject); 87 | } else { 88 | if (err) { 89 | reject( 90 | Error(`\`${command}\` exited with exit code ${err.code}. 91 | Consult the [Extensions Output](https://github.com/haskell/vscode-haskell#investigating-and-reporting-problems) 92 | for details.`), 93 | ); 94 | } else { 95 | resolve(stdout?.trim()); 96 | } 97 | } 98 | }, 99 | ) 100 | .on('exit', (code, signal) => { 101 | const msg = 102 | `Execution of '${command}' terminated with code ${code}` + (signal ? `and signal ${signal}` : ''); 103 | logger.log(msg); 104 | }) 105 | .on('error', (err) => { 106 | if (err) { 107 | logger.error(`Error executing '${command}': name = ${err.name}, message = ${err.message}`); 108 | reject(err); 109 | } 110 | }); 111 | token.onCancellationRequested(() => childProcess.kill()); 112 | }); 113 | }, 114 | ); 115 | } 116 | 117 | /** 118 | * Compare the PVP versions of two strings. 119 | * Details: https://github.com/haskell/pvp/ 120 | * 121 | * @param l First version 122 | * @param r second version 123 | * @returns `1` if l is newer than r, `0` if they are equal and `-1` otherwise. 124 | */ 125 | export function comparePVP(l: string, r: string): number { 126 | const al = l.split('.'); 127 | const ar = r.split('.'); 128 | 129 | let eq = 0; 130 | 131 | for (let i = 0; i < Math.max(al.length, ar.length); i++) { 132 | const el = parseInt(al[i], 10) || undefined; 133 | const er = parseInt(ar[i], 10) || undefined; 134 | 135 | if (el === undefined && er === undefined) { 136 | break; 137 | } else if (el !== undefined && er === undefined) { 138 | eq = 1; 139 | break; 140 | } else if (el === undefined && er !== undefined) { 141 | eq = -1; 142 | break; 143 | } else if (el !== undefined && er !== undefined && el > er) { 144 | eq = 1; 145 | break; 146 | } else if (el !== undefined && er !== undefined && el < er) { 147 | eq = -1; 148 | break; 149 | } 150 | } 151 | return eq; 152 | } 153 | 154 | /** When making http requests to github.com, use this header otherwise 155 | * the server will close the request 156 | */ 157 | const userAgentHeader = { 'User-Agent': 'vscode-haskell' }; 158 | 159 | export async function httpsGetSilently(options: https.RequestOptions): Promise { 160 | const opts: https.RequestOptions = { 161 | ...options, 162 | headers: { 163 | ...(options.headers ?? {}), 164 | ...userAgentHeader, 165 | }, 166 | }; 167 | 168 | return new Promise((resolve, reject) => { 169 | let data = ''; 170 | https 171 | .get(opts, (res) => { 172 | if (res.statusCode === 301 || res.statusCode === 302) { 173 | if (!res.headers.location) { 174 | reject(new Error('301/302 without a location header')); 175 | return; 176 | } 177 | https.get(res.headers.location, (resAfterRedirect) => { 178 | resAfterRedirect.on('data', (d) => (data += d)); 179 | resAfterRedirect.on('error', reject); 180 | resAfterRedirect.on('close', () => { 181 | resolve(data); 182 | }); 183 | }); 184 | } else if (!res.statusCode || res.statusCode >= 400) { 185 | reject(new Error(`Unexpected status code: ${res.statusCode}`)); 186 | } else { 187 | res.on('data', (d) => (data += d)); 188 | res.on('error', reject); 189 | res.on('close', () => { 190 | resolve(data); 191 | }); 192 | } 193 | }) 194 | .on('error', reject); 195 | }); 196 | } 197 | 198 | /** 199 | * Checks if the executable is on the PATH 200 | * @param exe Name of the executable to find. Caller must ensure '.exe' extension is included on windows. 201 | */ 202 | export function executableExists(exe: string): boolean { 203 | const isWindows = process.platform === 'win32'; 204 | let newEnv: IEnvVars = resolveServerEnvironmentPATH( 205 | workspace.getConfiguration('haskell').get('serverEnvironment') || {}, 206 | ); 207 | newEnv = { ...(process.env as IEnvVars), ...newEnv }; 208 | const cmd: string = isWindows ? 'where' : 'which'; 209 | const out = child_process.spawnSync(cmd, [exe], { env: newEnv }); 210 | return out.status === 0 || (which.sync(exe, { nothrow: true, path: newEnv.PATH }) ?? '') !== ''; 211 | } 212 | 213 | export function directoryExists(path: string): boolean { 214 | return fs.existsSync(path) && fs.lstatSync(path).isDirectory(); 215 | } 216 | 217 | export function expandHomeDir(path: string): string { 218 | if (path.startsWith('~')) { 219 | return path.replace('~', os.homedir); 220 | } 221 | return path; 222 | } 223 | 224 | export function resolvePathPlaceHolders(path: string, folder?: WorkspaceFolder) { 225 | path = path.replace('${HOME}', os.homedir).replace('${home}', os.homedir).replace(/^~/, os.homedir); 226 | if (folder) { 227 | path = path.replace('${workspaceFolder}', folder.uri.path).replace('${workspaceRoot}', folder.uri.path); 228 | } 229 | return path; 230 | } 231 | 232 | export function resolvePATHPlaceHolders(path: string) { 233 | return path 234 | .replace('${HOME}', os.homedir) 235 | .replace('${home}', os.homedir) 236 | .replace('$PATH', process.env.PATH ?? '$PATH') 237 | .replace('${PATH}', process.env.PATH ?? '${PATH}'); 238 | } 239 | 240 | // also honours serverEnvironment.PATH 241 | export function addPathToProcessPath(extraPath: string): string { 242 | const pathSep = process.platform === 'win32' ? ';' : ':'; 243 | const serverEnvironment: IEnvVars = workspace.getConfiguration('haskell').get('serverEnvironment') || {}; 244 | const path: string[] = serverEnvironment.PATH 245 | ? serverEnvironment.PATH.split(pathSep).map((p) => resolvePATHPlaceHolders(p)) 246 | : (process.env.PATH?.split(pathSep) ?? []); 247 | path.unshift(extraPath); 248 | return path.join(pathSep); 249 | } 250 | 251 | export function resolveServerEnvironmentPATH(serverEnv: IEnvVars): IEnvVars { 252 | const pathSep = process.platform === 'win32' ? ';' : ':'; 253 | const path: string[] | null = serverEnv.PATH 254 | ? serverEnv.PATH.split(pathSep).map((p) => resolvePATHPlaceHolders(p)) 255 | : null; 256 | return { 257 | ...serverEnv, 258 | ...(path ? { PATH: path.join(pathSep) } : {}), 259 | }; 260 | } 261 | -------------------------------------------------------------------------------- /test/suite/extension.test.ts: -------------------------------------------------------------------------------- 1 | // We have the following testing targets: 2 | // 1. Test if the extension is present 3 | // 2. Test if the extension can be activated 4 | // 3. Test if the extension can create the extension log file 5 | // 4. Test if the extension log contains server output (currently we use this to ensure the server is activated successfully) 6 | // 5. Test if the server inherit environment variables defined in the settings 7 | 8 | import * as vscode from 'vscode'; 9 | import * as assert from 'assert'; 10 | import * as path from 'path'; 11 | import * as fs from 'fs'; 12 | import { StopServerCommandName } from '../../src/commands/constants'; 13 | 14 | const LOG = 'hls.log'; 15 | const CACHE = 'cache-test'; 16 | const BIN = 'bin'; 17 | type AllowedKeys = typeof LOG | typeof CACHE; 18 | 19 | suite('Extension Test Suite', () => { 20 | const extension: vscode.Extension | undefined = vscode.extensions.getExtension('haskell.haskell'); 21 | const haskellConfig = vscode.workspace.getConfiguration('haskell'); 22 | 23 | suiteSetup(async () => { 24 | await haskellConfig.update('promptBeforeDownloads', false, vscode.ConfigurationTarget.Global); 25 | await haskellConfig.update('manageHLS', 'GHCup'); 26 | await haskellConfig.update('logFile', LOG); 27 | await haskellConfig.update('trace.server', 'messages'); 28 | await haskellConfig.update('releasesDownloadStoragePath', path.normalize(getWorkspaceFile(BIN).fsPath)); 29 | await haskellConfig.update('serverEnvironment', { 30 | XDG_CACHE_HOME: path.normalize(getWorkspaceFile(CACHE).fsPath), 31 | }); 32 | 33 | const contents = new TextEncoder().encode('main = putStrLn "hi vscode tests"'); 34 | await vscode.workspace.fs.writeFile(getWorkspaceFile('Main.hs'), contents); 35 | }); 36 | 37 | test('1. Extension should be present', () => { 38 | assert.ok(extension); 39 | }); 40 | 41 | test('2. Extension can be activated', async () => { 42 | assert.ok(await extension?.activate().then(() => true)); 43 | }); 44 | 45 | test('3. Extension should create the extension log file', async () => { 46 | // Open the document to trigger the extension 47 | vscode.workspace.openTextDocument(getWorkspaceFile('Main.hs')); 48 | assert.ok(await runWithIntervalAndTimeout(() => workspaceFileExist(LOG), 1, 60)); 49 | }); 50 | 51 | test('4. Extension log should have server output', async () => { 52 | vscode.workspace.openTextDocument(getWorkspaceFile('Main.hs')); 53 | const checkServerLog = () => { 54 | const logContents = getExtensionLogContent(); 55 | if (logContents) { 56 | return logContents.match(/Registering IDE configuration/i) !== null; 57 | } 58 | return false; 59 | }; 60 | assert.ok(await runWithIntervalAndTimeout(checkServerLog, 1, 60), 'Extension log file has no expected hls output'); 61 | }); 62 | 63 | test('5. Server should inherit environment variables defined in the settings', async () => { 64 | vscode.workspace.openTextDocument(getWorkspaceFile('Main.hs')); 65 | assert.ok( 66 | await runWithIntervalAndTimeout(() => workspaceFileExist(CACHE), 1, 30), 67 | 'Server did not inherit XDG_CACHE_DIR from environment variables set in the settings', 68 | ); 69 | }); 70 | 71 | suiteTeardown(async () => { 72 | console.log('Stopping the lsp server'); 73 | await vscode.commands.executeCommand(StopServerCommandName); 74 | 75 | console.log('Contents of the extension log:'); 76 | const logContents = getExtensionLogContent(); 77 | if (logContents) { 78 | console.log(logContents); 79 | } 80 | }); 81 | }); 82 | 83 | ////////////////////////// 84 | // Helper functions BEGIN 85 | ////////////////////////// 86 | 87 | function getWorkspaceRoot(): vscode.WorkspaceFolder { 88 | const folders = vscode.workspace.workspaceFolders; 89 | if (folders) { 90 | return folders[0]; 91 | } else { 92 | throw Error('workspaceFolders is empty'); 93 | } 94 | } 95 | 96 | function getWorkspaceFile(name: string): vscode.Uri { 97 | const wsroot = getWorkspaceRoot().uri; 98 | return wsroot.with({ path: path.posix.join(wsroot.path, name) }); 99 | } 100 | 101 | /** 102 | * Check if the given file exists in the workspace. 103 | * @param key The key name 104 | * @returns `True` if exists, otherwise `False` 105 | */ 106 | function workspaceFileExist(key: AllowedKeys): boolean { 107 | const folder = getWorkspaceRoot(); 108 | const targetPath = path.join(folder.uri.fsPath, key); 109 | 110 | return fs.existsSync(targetPath); 111 | } 112 | 113 | /** 114 | * Run a function by given interval and timeout. 115 | * @param fn The function to run, which has the signature `() => boolean` 116 | * @param interval Interval in seconds 117 | * @param timeout Interval in seconds 118 | * @returns `true` if `fn` returns `true` before the `timeout`, otherwise `false` 119 | */ 120 | async function runWithIntervalAndTimeout(fn: () => boolean, interval: number, timeout: number): Promise { 121 | const startTime = Date.now(); 122 | const intervalMs = interval * 1000; 123 | const timeoutMs = timeout * 1000; 124 | const endTime = startTime + timeoutMs; 125 | const wait = (ms: number) => new Promise((r) => setTimeout(r, ms)); 126 | 127 | while (Date.now() <= endTime) { 128 | if (fn()) { 129 | return true; 130 | } 131 | await wait(intervalMs); 132 | } 133 | 134 | return false; 135 | } 136 | 137 | function getExtensionLogContent(): string | undefined { 138 | const extLog = getWorkspaceFile(LOG).fsPath; 139 | if (fs.existsSync(extLog)) { 140 | const logContents = fs.readFileSync(extLog); 141 | return logContents.toString(); 142 | } else { 143 | console.log(`${extLog} does not exist!`); 144 | return undefined; 145 | } 146 | } 147 | 148 | ////////////////////////// 149 | // Helper functions END 150 | ////////////////////////// 151 | -------------------------------------------------------------------------------- /test/suite/index.ts: -------------------------------------------------------------------------------- 1 | import * as glob from 'glob'; 2 | import * as Mocha from 'mocha'; 3 | import * as path from 'path'; 4 | 5 | export async function run(): Promise { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: 'tdd', 9 | timeout: 210_000, // 3.5 mins 10 | color: true, 11 | }); 12 | 13 | const testsRoot = path.resolve(__dirname, '..'); 14 | 15 | return new Promise((c, e) => { 16 | glob 17 | .glob('**/**.test.js', { cwd: testsRoot }) 18 | .then((files) => { 19 | // Add files to the test suite 20 | files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))); 21 | 22 | try { 23 | // Run the mocha test 24 | mocha.run((failures) => { 25 | if (failures > 0) { 26 | e(new Error(`${failures} tests failed.`)); 27 | } else { 28 | c(); 29 | } 30 | }); 31 | } catch (err) { 32 | console.error(err); 33 | e(err); 34 | } 35 | }) 36 | .catch((err) => e(err)); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "CommonJS", 4 | "target": "es2022", 5 | "outDir": "out", 6 | "lib": ["es2022"], 7 | "sourceMap": true, 8 | "rootDir": ".", 9 | "noUnusedLocals": true, 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "noImplicitReturns": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "strictNullChecks": true, 15 | "strictBuiltinIteratorReturn": false 16 | }, 17 | "include": ["./src/**/*.ts", "./test/**/*.ts"], 18 | "exclude": ["node_modules", ".vscode", ".vscode-test"] 19 | } 20 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | 6 | //@ts-check 7 | 8 | 'use strict'; 9 | 10 | const path = require('path'); 11 | const ESLintPlugin = require('eslint-webpack-plugin'); 12 | 13 | /**@type {import('webpack').Configuration}*/ 14 | const config = { 15 | target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 16 | entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 17 | output: { 18 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 19 | path: path.resolve(__dirname, 'dist'), 20 | filename: 'extension.js', 21 | libraryTarget: 'commonjs2', 22 | devtoolModuleFilenameTemplate: '../[resource-path]' 23 | }, 24 | devtool: 'source-map', 25 | externals: { 26 | vscode: 'commonjs vscode' // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 27 | }, 28 | resolve: { 29 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 30 | extensions: ['.ts', '.js'] 31 | }, 32 | plugins: [new ESLintPlugin()], 33 | module: { 34 | rules: [ 35 | { 36 | test: /\.ts$/, 37 | exclude: /node_modules/, 38 | enforce: 'pre', 39 | use: [ 40 | { 41 | loader: 'ts-loader' 42 | } 43 | ] 44 | } 45 | ] 46 | } 47 | }; 48 | 49 | module.exports = config; 50 | --------------------------------------------------------------------------------