Showing preview only (203K chars total). Download the full file or copy to clipboard to get everything.
Repository: alanz/vscode-hie-server
Branch: master
Commit: 5e2d6611d333
Files: 40
Total size: 191.7 KB
Directory structure:
gitextract_23g9bkdj/
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.md
│ │ └── feature_request.md
│ ├── dependabot.yml
│ └── workflows/
│ ├── package.yml
│ ├── release.yml
│ └── test.yml
├── .gitignore
├── .prettierignore
├── .prettierrc
├── .vscode/
│ ├── extensions.json
│ ├── launch.json
│ ├── settings.json
│ └── tasks.json
├── .vscode-test.js
├── .vscodeignore
├── Changelog.md
├── GenChangelogs.hs
├── LICENSE
├── README.md
├── docs/
│ ├── Contributing.md
│ └── Release.md
├── eslint.config.mjs
├── flake.nix
├── 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
├── test-workspace/
│ └── .gitkeep
├── tsconfig.json
└── webpack.config.js
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: 'status: needs triage'
assignees: ''
---
<!--
Before opening an issue, please take a look at the [troubleshooting guide](https://github.com/haskell/vscode-haskell#troubleshooting). This explains some common issues and will also help you to find the information that the issue template asks for.
When filing an issue, please fill out as much of the information below as you can. This helps us to debug your issue, but is not required!
-->
### Your environment
Which OS do you use:
<!-- Windows, MacOS, Ubuntu, ArchLinux, etc... -->
### Steps to reproduce
<!-- Tell us how to reproduce this issue, including screenshots if you think they can be useful -->
### Expected behaviour
<!-- Tell us what should happen. -->
### Actual behaviour
<!-- Tell us what happens instead. -->
### Include debug information
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)):
<details>
<summary>
Debug output:
</summary>
```
<paste your logs here>
```
</details>
Paste the contents of extension specific log, you can check instructions about how to find it [here](https://github.com/haskell/vscode-haskell#troubleshooting)
<details>
<summary>
Extension log:
</summary>
```
<paste your logs here>
```
</details>
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
**Describe the solution you'd like**
<!-- A clear and concise description of what you want to happen. -->
**Describe alternatives you've considered**
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
**Additional context**
<!-- Add any other context or screenshots about the feature request here. -->
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
# NOTE: Dependabot official configuration documentation:
# https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates#package-ecosystem
# Maintain dependencies for internal GitHub Actions CI for pull requests
- package-ecosystem: 'github-actions'
directory: '/'
schedule:
interval: 'weekly'
- package-ecosystem: 'npm'
directory: '/'
schedule:
interval: 'weekly'
================================================
FILE: .github/workflows/package.yml
================================================
on:
push:
branches:
- master
pull_request:
branches:
- '**'
jobs:
build:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install Node.js
uses: actions/setup-node@v6
with:
## make sure this corresponds with the version in release.yml
node-version: latest
- run: |
npm ci
- name: Package extension
run: npx vsce package
- name: Upload extension vsix to workflow artifacts
uses: actions/upload-artifact@v7
with:
name: haskell-${{ github.sha }}.vsix
path: haskell-*.vsix
================================================
FILE: .github/workflows/release.yml
================================================
on:
release:
types: [prereleased, released]
name: Deploy Extension
jobs:
publish-extension:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
## make sure this corresponds with the version in test.yml
node-version: latest
- run: npm ci
- name: Package Extension
id: packageExtension
uses: HaaLeo/publish-vscode-extension@v2
with:
pat: stub
dryRun: true
preRelease: ${{ github.event.action == 'prereleased' }}
## Make sure the artifact is added to the release.
- name: Upload extension vsix to workflow artifacts
uses: actions/upload-artifact@v7
with:
name: haskell-${{ github.event.release.tag_name }}.vsix
path: ${{ steps.packageExtension.outputs.vsixPath }}
## If this is a release job, publish to VSCode Marketplace,
## otherwise publish a pre-release to VSCode Marketplace
- name: Publish to Visual Studio Marketplace
id: publishToVSMarketplace
uses: HaaLeo/publish-vscode-extension@v2
with:
pat: ${{ secrets.VS_MARKETPLACE_TOKEN }}
registryUrl: https://marketplace.visualstudio.com
extensionFile: ${{ steps.packageExtension.outputs.vsixPath }}
preRelease: ${{ github.event.action == 'prereleased' }}
## If this is a release job, publish to VSX Marketplace,
## otherwise publish a pre-release to VSX Marketplace
- name: Publish to Open VSX Registry
id: publishToOpenVSX
continue-on-error: true
uses: HaaLeo/publish-vscode-extension@v2
with:
pat: ${{ secrets.OPEN_VSX_TOKEN }}
extensionFile: ${{ steps.packageExtension.outputs.vsixPath }}
preRelease: ${{ github.event.action == 'prereleased' }}
================================================
FILE: .github/workflows/test.yml
================================================
on:
push:
branches:
- master
pull_request:
branches:
- '**'
jobs:
build:
strategy:
fail-fast: false
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
ghc: [8.10.7, 9.6.7, 9.8.4, 9.12.2]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Install Node.js
uses: actions/setup-node@v6
with:
## make sure this corresponds with the version in release.yml
node-version: latest
# Install test dependencies
- run: npm ci
- run: npm run webpack
# Setup toolchains, install ghcup, install ghc, etc...
- name: Install GHCup
run: |
curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh
shell: bash
env:
BOOTSTRAP_HASKELL_NONINTERACTIVE: 1
BOOTSTRAP_HASKELL_MINIMAL: 1
- name: Check GHCup (Windows)
run: |
echo "c:/ghcup/bin" >> $GITHUB_PATH
shell: bash
if: runner.os == 'Windows'
- name: Check GHCup (Unix)
run: |
echo "${HOME}/.ghcup/bin" >> $GITHUB_PATH
shell: bash
if: runner.os != 'Windows'
- name: Toolchain settings
run: |
ghcup upgrade -i -f
export GHCUP_INSTALL_BASE_PREFIX=$(pwd)/test-workspace/bin
ghcup config set cache true
ghcup install stack latest
ghcup install cabal latest
ghcup install ghc ${{ matrix.ghc }}
ghcup set ghc ${{ matrix.ghc }}
# This is a prefetched, fallback HLS version.
# We want to make sure, we still support old GHC versions
# and graciously fallback to an HLS version that supports the old GHC version, such as 8.10.7
ghcup install hls 2.2.0.0
ghcup install hls latest
shell: bash
# Run the tests
- name: Run the test on Linux
run: |
export GHCUP_INSTALL_BASE_PREFIX=$(pwd)/test-workspace/bin
export PATH="$(pwd)/test-workspace/bin/.ghcup/bin:$PATH"
xvfb-run -s '-screen 0 640x480x16' -a npm run test
shell: bash
if: runner.os == 'Linux'
- name: Run the test on macOS
run: |
export GHCUP_INSTALL_BASE_PREFIX=$(pwd)/test-workspace/bin
export PATH="$(pwd)/test-workspace/bin/.ghcup/bin:$PATH"
npm run test
shell: bash
if: runner.os == 'macOS'
- name: Run the test on Windows
run: |
export GHCUP_INSTALL_BASE_PREFIX=$(pwd)/test-workspace/bin
export PATH="$(pwd)/test-workspace/bin/ghcup/bin:$PATH"
npm run test
shell: bash
if: runner.os == 'Windows'
# Create package artefacts
- name: Delete test artefacts
# The test-suite doesn't clean up correctly after itself.
# This is a poor man's workaround that after test execution,
# the test-workspace still contains binaries and caches.
run: |
rm -rf test-workspace
rm -rf out
shell: bash
================================================
FILE: .gitignore
================================================
out
node_modules
.vscode-test
.DS_Store
dist
*.vsix
.husky
# Ignore everything in test-workspace
test-workspace/*
# Except the .gitkeep file
!test-workspace/.gitkeep
================================================
FILE: .prettierignore
================================================
node_modules/
test-workspace/
.vscode/
.vscode-test/
out/
dist/
webpack.config.js
================================================
FILE: .prettierrc
================================================
printWidth: 120
singleQuote: true
================================================
FILE: .vscode/extensions.json
================================================
{
"recommendations": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode"
]
}
================================================
FILE: .vscode/launch.json
================================================
// A launch configuration that compiles the extension and then opens it inside a new window
{
"version": "0.2.0",
"configurations": [
{
"name": "Extension",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": ["--extensionDevelopmentPath=${workspaceRoot}"],
"sourceMaps": true,
"outFiles": ["${workspaceRoot}/dist/**/*.js"],
"preLaunchTask": "npm: webpack"
},
{
"name": "Extension Tests",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"testConfiguration": "${workspaceFolder}/.vscode-test.js",
"args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/out/test"],
"sourceMaps": true,
"outFiles": ["${workspaceRoot}/out/test/**/*.js"],
"preLaunchTask": "npm: pretest"
}
]
}
================================================
FILE: .vscode/settings.json
================================================
// Place your settings in this file to overwrite default and user settings.
{
"files.exclude": {
"dist": true, // set this to true to hide the "dist" folder with the compiled JS files
".vscode-test": true,
"node_modules": true
},
"search.exclude": {
"dist": true // set this to false to include "dist" folder in search results
},
"typescript.tsdk": "./node_modules/typescript/lib",
"editor.tabSize": 2, // we want to use the TS server from our node_modules folder to control its version,
"editor.formatOnSave": true,
"files.associations": {
".prettierrc": "yaml"
},
"files.eol": "\n",
"haskell.formattingProvider": "stylish-haskell"
}
================================================
FILE: .vscode/tasks.json
================================================
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "watch",
"problemMatcher": {
"owner": "typescript",
"applyTo": "closedDocuments",
"fileLocation": ["absolute"],
"pattern": {
"regexp": "<nothing>"
},
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "Compilation (.*?)starting…"
},
"endsPattern": {
"regexp": "Compilation (.*?)finished"
}
}
},
"isBackground": true,
"presentation": {
"reveal": "never"
},
"group": {
"kind": "build",
"isDefault": true
}
},
{
"type": "npm",
"script": "test",
"group": {
"kind": "test",
"isDefault": true
}
}
]
}
================================================
FILE: .vscode-test.js
================================================
const { defineConfig } = require('@vscode/test-cli');
module.exports = defineConfig([
{
label: 'integration-tests',
files: 'out/test/**/*.test.js',
version: 'stable',
workspaceFolder: './test-workspace',
installExtensions: ['justusadam.language-haskell'],
mocha: {
timeout: 120 * 1000, // 2 minute timeout
},
},
// you can specify additional test configurations, too
]);
================================================
FILE: .vscodeignore
================================================
.vscode/**
.vscode-test/**
typings/**
out/test/**
test/**
src/**
**/*.map
.gitignore
tsconfig.json
node_modules
out
src
webpack.config.json
================================================
FILE: Changelog.md
================================================
# Changelog for vscode-haskell
## 2.8.0
- Migrate project to npm 11.9.0
([#1336](https://github.com/haskell/vscode-haskell/pull/1336)) by @fendor
- Standardized LICENSE for vscode-haskell
([#1290](https://github.com/haskell/vscode-haskell/pull/1290)) by @Shubhashish-Chakraborty
## 2.7.0
- Introduce the `StatusBarItem`
([#1237](https://github.com/haskell/vscode-haskell/pull/1237)) by @fendor
## 2.6.1
- Prefer the `set` version for `cabal` and `stack` if there is any
([#1275](https://github.com/haskell/vscode-haskell/pull/1275)) by @fendor
- Make js debugger work
([#1258](https://github.com/haskell/vscode-haskell/pull/1258)) by @dyniec
- Prepare release 2.6.0
([#1103](https://github.com/haskell/vscode-haskell/pull/1103)) by @fendor
## 2.6.0
- Add option to enable/disable `.cabal` file support
([#1223](https://github.com/haskell/vscode-haskell/pull/1223)) by @fendor
- Upgrade project to use latest eslint version
([#1150](https://github.com/haskell/vscode-haskell/pull/1150)) by @fendor
- Fix windows CI
([#1149](https://github.com/haskell/vscode-haskell/pull/1149)) by @fendor
- Manually install ghcup into image
([#1119](https://github.com/haskell/vscode-haskell/pull/1119)) by @fendor
- bump vscode-languageclient version to 9.0.1
([#1108](https://github.com/haskell/vscode-haskell/pull/1108)) by @jetjinser
- Add cabalFormattingProvider to package.json
([#1100](https://github.com/haskell/vscode-haskell/pull/1100)) by @fendor
## 2.5.3
- Split out packaging action
([#1080](https://github.com/haskell/vscode-haskell/pull/1080)) by @fendor
- Add Session Loading style to list of known configs
([#1077](https://github.com/haskell/vscode-haskell/pull/1077)) by @fendor
- Tooling update
([#1043](https://github.com/haskell/vscode-haskell/pull/1043)) by @bzm3r
- Add `haskell.plugin.fourmolu.config.path` option
([#987](https://github.com/haskell/vscode-haskell/pull/987)) by @georgefst
## 2.5.2
- Includes changes of the 2.4.3 release
## 2.5.1
- Includes changes of the 2.4.2 release
## 2.5.0
- Add tracking of cabal files
([#618](https://github.com/haskell/vscode-haskell/pull/618)) by @fendor
## 2.4.3
- Address invalid byte sequence error #1022
([#1022](https://github.com/haskell/vscode-haskell/pull/1022)) by @felixlinker
- Always set the cwd for the executable (#1011)
([#1011](https://github.com/haskell/vscode-haskell/pull/1011)) by @fendor
## 2.4.2
- Add stan plugin option #1000
([#1000](https://github.com/haskell/vscode-haskell/pull/1000)) by @fendor
- Probe for GHCup binary wrt #962
([#963](https://github.com/haskell/vscode-haskell/pull/963)) by @hasufell
- Bump old hls version and upgrade test runner to macos-latest
([#960](https://github.com/haskell/vscode-haskell/pull/960)) by @July541
- Increase time limitation to make test on Windows more stable
([#959](https://github.com/haskell/vscode-haskell/pull/959)) by @July541
- Update release docs for refreshing CI tokens
([#942](https://github.com/haskell/vscode-haskell/pull/942)) by @fendor
## 2.4.1
- Downgrade vscode-languageclient
([#934](https://github.com/haskell/vscode-haskell/pull/934)) by @fendor
- Bump vscode to 1.80.0
([#912](https://github.com/haskell/vscode-haskell/pull/912)) by @July541
## 2.4.0
- Prepare release 2.4.0
([#906](https://github.com/haskell/vscode-haskell/pull/906)) by @VeryMilkyJoe
- Simplify tests
([#904](https://github.com/haskell/vscode-haskell/pull/904)) by @July541
- Remove unused code
([#898](https://github.com/haskell/vscode-haskell/pull/898)) by @fendor
- Remove hoogle command from vscode extension
([#896](https://github.com/haskell/vscode-haskell/pull/896)) by @fendor
- Update readme
([#886](https://github.com/haskell/vscode-haskell/pull/886)) by @VeryMilkyJoe
- Fix broken tests
([#880](https://github.com/haskell/vscode-haskell/pull/880)) by @July541
- Update README.md: clarify how to use Stack with vscode-haskell extension
([#874](https://github.com/haskell/vscode-haskell/pull/874)) by @miguel-negrao
- Remove debugger tools from CI
([#873](https://github.com/haskell/vscode-haskell/pull/873)) by @fendor
- Refactor tests to work correctly
([#872](https://github.com/haskell/vscode-haskell/pull/872)) by @July541
- Downgrade vscode language client to 7.0.0
([#853](https://github.com/haskell/vscode-haskell/pull/853)) by @fendor
- Update badge url for VSCode Marketplace
([#851](https://github.com/haskell/vscode-haskell/pull/851)) by @fendor
## 2.2.4
- Downgrade vscode language client to 7.0.0
([#843](https://github.com/haskell/vscode-haskell/pull/853)) by @fendor
## 2.2.3
- Prepare release 2.2.3
([#843](https://github.com/haskell/vscode-haskell/pull/843)) by @fendor
- Add new plugins fields
([#842](https://github.com/haskell/vscode-haskell/pull/842)) by @fendor
- Migrate to eslint
([#782](https://github.com/haskell/vscode-haskell/pull/782)) by @fendor
- Bump minor versions of package dependencies
([#781](https://github.com/haskell/vscode-haskell/pull/781)) by @fendor
- Update unsupported GHC doc link
([#776](https://github.com/haskell/vscode-haskell/pull/776)) by @limaak
- Fix release CI
([#775](https://github.com/haskell/vscode-haskell/pull/775)) by @fendor
- Fix mistake in generated ChangeLog
([#774](https://github.com/haskell/vscode-haskell/pull/774)) by @fendor
## 2.2.2
- Add link to HLS installation webpage
([#751](https://github.com/haskell/vscode-haskell/pull/751)) by @fendor
- Change scope of serverExecutablePath to machine-overridable
([#742](https://github.com/haskell/vscode-haskell/pull/742)) by @fendor
- Add Fourmolu config property
([#736](https://github.com/haskell/vscode-haskell/pull/736)) by @georgefst
- Add missing configuration options for the latest HLS version
([#717](https://github.com/haskell/vscode-haskell/pull/717)) by @fendor
- Change sensible to sensitive
([#709](https://github.com/haskell/vscode-haskell/pull/709)) by @ploeh
## 2.2.1
- Fix test-suite for new GHCUp release
([#672](https://github.com/haskell/vscode-haskell/pull/672)) by @fendor
- Bump webpack from 5.73.0 to 5.74.0
([#657](https://github.com/haskell/vscode-haskell/pull/657)) by @fendor
- Bump typescript from 4.4.0 to 4.7.4
([#657](https://github.com/haskell/vscode-haskell/pull/657)) by @fendor
- Bump @types/node from 18.0.4 to 18.6.1
([#657](https://github.com/haskell/vscode-haskell/pull/657)) by @fendor
- Bump @typescript-eslint/eslint-plugin from 5.30.6 to 5.31.0
([#657](https://github.com/haskell/vscode-haskell/pull/657)) by @fendor
- Bump @typescript-eslint/parser from 5.30.6 to 5.31.0
([#657](https://github.com/haskell/vscode-haskell/pull/657)) by @fendor
- Bump prettier from 2.6.2 to 2.7.1
([#657](https://github.com/haskell/vscode-haskell/pull/657)) by @fendor
- Bump mocha from 9.2.1 to 10.0.0
([#657](https://github.com/haskell/vscode-haskell/pull/657)) by @fendor
- Add dependabot.yml
([#633](https://github.com/haskell/vscode-haskell/pull/633)) by @fendor
- Replace x32 with ia32 for Architecture matching
([#631](https://github.com/haskell/vscode-haskell/pull/631)) by @fendor
- Toolchain management dialog: add hint for beginners
([#621](https://github.com/haskell/vscode-haskell/pull/621)) by @runeksvendsen
- Fix trace.server option
([#617](https://github.com/haskell/vscode-haskell/pull/617)) by @coltenwebb
- Add TOC
([#615](https://github.com/haskell/vscode-haskell/pull/615)) by @hasufell
- Cleanups
([#605](https://github.com/haskell/vscode-haskell/pull/605)) by @hasufell
- Link to VSCode settings page
([#603](https://github.com/haskell/vscode-haskell/pull/603)) by @hasufell
- Refactor toInstall shenanigans
([#600](https://github.com/haskell/vscode-haskell/pull/600)) by @hasufell
- Fix confusing download dialog popup
([#599](https://github.com/haskell/vscode-haskell/pull/599)) by @hasufell
- More troubleshooting
([#598](https://github.com/haskell/vscode-haskell/pull/598)) by @hasufell
## 2.2.0
- Bump version to 2.2.0 (Syncs up pre-release and release version)
([#594](https://github.com/haskell/vscode-haskell/pull/594)) by @fendor
## 2.0.1
- Bad error message when ghcup is not installed
([#591](https://github.com/haskell/vscode-haskell/pull/591)) by @hasufell
- Better error message if we can't find a HLS version for a given GHC
([#588](https://github.com/haskell/vscode-haskell/pull/588)) by @hasufell
- Properly convert release metadata from json
([#585](https://github.com/haskell/vscode-haskell/pull/585)) by @fendor
- Ignore missing entries in Release Metadata
([#582](https://github.com/haskell/vscode-haskell/pull/582)) by @fendor
- Add Tool class and print stacktraces
([#579](https://github.com/haskell/vscode-haskell/pull/579)) by @fendor
- List Env Vars we care about only
([#578](https://github.com/haskell/vscode-haskell/pull/578)) by @fendor
- Prepare pre-release 2.1.0
([#574](https://github.com/haskell/vscode-haskell/pull/574)) by @fendor
- Enable pre-release feature for VSX Marketplace
([#573](https://github.com/haskell/vscode-haskell/pull/573)) by @fendor
- Add prettier script
([#566](https://github.com/haskell/vscode-haskell/pull/566)) by @fendor
- Remove accidental run command
([#565](https://github.com/haskell/vscode-haskell/pull/565)) by @fendor
- Upgrade dependencies
([#564](https://github.com/haskell/vscode-haskell/pull/564)) by @fendor
- Add new configuration options for rename plugin
([#563](https://github.com/haskell/vscode-haskell/pull/563)) by @OliverMadine
- Introduce 'haskell.toolchain' setting
([#562](https://github.com/haskell/vscode-haskell/pull/562)) by @hasufell
- Improve
([#558](https://github.com/haskell/vscode-haskell/pull/558)) by @hasufell
- Remove stdout/sterr from user error message
([#556](https://github.com/haskell/vscode-haskell/pull/556)) by @fendor
- Fix npm security issue
([#555](https://github.com/haskell/vscode-haskell/pull/555)) by @fendor
- No colour output for GHCup
([#554](https://github.com/haskell/vscode-haskell/pull/554)) by @fendor
- Add eval plugin configuration
([#549](https://github.com/haskell/vscode-haskell/pull/549)) by @xsebek
- Manage all the Haskell things
([#547](https://github.com/haskell/vscode-haskell/pull/547)) by @hasufell
- Consider user installed HLSes (e.g. via ghcup compile)
([#543](https://github.com/haskell/vscode-haskell/pull/543)) by @hasufell
- Update README.MD GHC support
([#537](https://github.com/haskell/vscode-haskell/pull/537)) by @cptwunderlich
- fix: change deprecated Haskell Platform install link to GHCup
([#536](https://github.com/haskell/vscode-haskell/pull/536)) by @HEIGE-PCloud
- Update HLS installation method
([#533](https://github.com/haskell/vscode-haskell/pull/533)) by @hasufell
- Fixes related with paths
([#518](https://github.com/haskell/vscode-haskell/pull/518)) by @jneira
- Reorganize troubleshooting section
([#516](https://github.com/haskell/vscode-haskell/pull/516)) by @jneira
## 1.8.0
This release includes some interesting new features:
- You can now pass custom environment variables to the lsp server
with the `haskell.serverEnvironment` config option per project basis,
thanks to [@jacobprudhomme](https://github.com/jacobprudhomme).
- For example: `"haskell.serverEnvironment": { "XDG_CACHE_HOME": "/path/to/my/cache" }`
- With this version the extension will try to use the newer lsp server version
which supports the ghc used by the project being loaded, thanks to [@mduerig](https://github.com/mduerig)
- WARNING: This will suppose it will use an older version than the latest one,
without its features and bug fixes.
- The extension has lot of more log traces now, which hopefully will help to
identify the cause of issues
### Pull requests merged for 1.8.0
- Update supported ghc versions for hls-1.5.1
([#514](https://github.com/haskell/vscode-haskell/pull/514)) by @jneira
- Fix hole_severity option: Use integer instead of string
([#511](https://github.com/haskell/vscode-haskell/pull/511)) by @mirko-plowtech
- Update issue templates
([#509](https://github.com/haskell/vscode-haskell/pull/509)) by @jneira
- Add traces for download hls
([#508](https://github.com/haskell/vscode-haskell/pull/508)) by @jneira
- support old hls versions compatible with the requested ghc version
([#506](https://github.com/haskell/vscode-haskell/pull/506)) by @mduerig
- Fix ci: ensure we have a supported ghc version in PATH
([#496](https://github.com/haskell/vscode-haskell/pull/496)) by @jneira
- Trace environment variables
([#495](https://github.com/haskell/vscode-haskell/pull/495)) by @jneira
- Pass environment variables to LSP
([#494](https://github.com/haskell/vscode-haskell/pull/494)) by @jacobprudhomme
- Reorganize README
([#491](https://github.com/haskell/vscode-haskell/pull/491)) by @jneira
- Fix error handling of server exec discovery in windows
([#486](https://github.com/haskell/vscode-haskell/pull/486)) by @jneira
- Bump versions of ts, cheerio, mocha
([#485](https://github.com/haskell/vscode-haskell/pull/485)) by @jneira
- Improve serverExecutablePath description and error when pointing to a directory
([#484](https://github.com/haskell/vscode-haskell/pull/484)) by @jneira
- Add integration smoke test
([#481](https://github.com/haskell/vscode-haskell/pull/481)) by @jneira
- Setup the test suite
([#475](https://github.com/haskell/vscode-haskell/pull/475)) by @jneira
## 1.7.1
- Bug fix release due to #471 and fixed with #469 thanks to [@berberman](https://github.com/berberman)
## 1.7.0
- Add an option to set server command line arguments thanks to [@cdsmith](https://github.com/cdsmith) <https://github.com/haskell/vscode-haskell/pull/464>
- It includes a new config option `haskell.serverExtraArgs` to being able to pass extra argument to the lsp server executable
- Update config options to match last haskell-language-server version <https://github.com/haskell/vscode-haskell/pull/463>
- It removes `haskell.diagnosticsOnChange` and `haskell.formatOnImportOn` cause they were unused in the server
- It adds `haskell.checkProject`, `haskell.maxCompletions` and `haskell.plugin.refineImports.globalOn`
- Fix showDocumentation command thanks to [@pranaysashank](https://github.com/pranaysashank) <https://github.com/haskell/vscode-haskell/pull/452>
- It fixes partially showing the documentation directly in vscode. The documentation is rendered but internal links still does not work
- Two config options has been added: `haskell.openDocumentationInHackage` and `haskell.openSourceInHackage` with default value `true`
- So documentation will be opened using the hackage url in an external navigator by default
- If you prefer having them in vscode you will need to change them to `false`
- Create output channel only if there are no existing clients thanks to [@pranaysashank](https://github.com/pranaysashank) <https://github.com/haskell/vscode-haskell/pull/448>
- This fixes the creation of several output channels for the extension
## 1.6.1
- Fix wrapper call to get project ghc version in windows with spaces in path (<https://github.com/haskell/vscode-haskell/pull/439>)
## 1.6.0
- Bump up vscode version to 1.52.0 (#424) by [@berberman](https://github.com/berberman)
- To match the lsp spec version used in haskell-language-version and fix <https://github.com/haskell/haskell-language-server/issues/2068>
## 1.5.1
- Add much more logging in the client side, configured with `haskell.trace.client`
- Fix error handling of `working out project ghc` and a bug when the path to the executable contains spaces (See #421)
- And dont use a shell to spawn the subprocess in non windows systems
- Show the progress as a cancellable notification
- Add commands `Start Haskell LSP server` and `Stop Haskell LSP server`
## 1.5.0
- Emit warning about limited support for ghc-9.x on hls executable download
- Fix `working out project ghc` progress notificacion
- Fix tactics config, thanks to @isovector
- Update server config to match haskell-language-server-1.3.0 one
## 1.4.0
- 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).
## 1.3.0
- Add `haskell.releasesURL` option to override where to look for HLS releases search for HLS downloads, thanks to @soiamsoNG
- With this version _the only supported lsp server variant is [`haskell-language-server`](https://github.com/haskell/haskell-language-server)_
- 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
- Add some plugin specic options:
- [wingman](https://haskellwingman.dev/) (aka tactics) plugin
- `haskell.plugin.tactic.config.features`: Feature set used by the plugin
- `haskell.plugin.tactics.config.hole_severity`: The severity to use when showing hole diagnostics
- `haskell.plugin.tactic.config.max_use_ctor_actions`: Maximum number of `Use constructor <x>` code actions that can appear
- `haskell.plugin.tactics.config.timeout_duration`: The timeout for Wingman actions, in seconds
- completions
- `haskell.plugin.ghcide-completions.config.autoExtendOn`: Extends the import list automatically when completing a out-of-scope identifier
- `haskell.plugin.ghcide-completions.config.snippetsOn`: Inserts snippets when using code completions
- type signature lenses - `haskell.plugin.ghcide-type-lenses.config.mode`: Control how type lenses are shown
- 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.
- Deprecated options:
- `haskell.hlintOn`: use `haskell.plugin.hlint.globalOn` instead.
- `haskell.completionSnippetsOn`: use `haskell.plugin.ghcide-completions.config.snippetsOn`
- Fixed a small typo that caused the server not to be loaded in `.lhs` files, thanks to @Max7cd
## 1.2.0
- Add option to open local documentation on Hackage (@DunetsNM)
- Add `haskell.updateBehaviour` option to configure when to check for updates
(@WorldSEnder)
- Use locally installed servers on connection failure (@WorldSEnder)
## 1.1.0
- Add Fourmolu as a plugin formatter provider (@georgefst)
- Remove the `haskell.enable` configuration option, since VS Code now allows
you to disable extensions on a per workspace basis
- Display errors when fetching from the GitHub API properly
## 1.0.1
- Switch the default formatter to Ormolu to match haskell-language-server
- Fix `haskell.serverExecutablePath` not working with absolute paths on Windows
(@winestone)
- Improve the help text and error message when `haskell.serverExecutablePath`
is not found
- Fix the rendering of the markdown table in the README (@Darren8098)
## 1.0.0
- vscode-haskell now lives under the Haskell organisation
- Can now download zip archived binaries, which the Windows binaries are now distributed as
- Improve README (@pepeiborra @jaspervdj)
## 0.1.1
- Fix the restart server and import identifier commands
## 0.1.0
`vscode-hie-server`/`Haskell Language Server` is now just Haskell, and will soon
be published under the Haskell organisation as `haskell-vscode`.
This release makes haskell-language-server the default langauge server of choice
and automatically downloads and installs binaries. Installation from source is
still supported though and any binaries located on your PATH for the selected
langauge server will be used instead.
### Important!
As part of this, your configuration may be reset as the keys move from
`languageServerHaskell.completionSnippetsOn` to `haskell.completionSnippetsOn`.
- Fix the document and source browser
- Remove obselete commands that are no longer supported by any of the language
servers
- Show type command
- Insert type command
- HaRe commands
- Case split commands
## 0.0.40
Change the way the backend is configured, simplifying it.
- remove wrapper scripts (hie-vscode.sh/hie-vscode.bat)
- dropdown choice between `haskell-ide-engine`, `haskell-language-server` or
`ghcide` in the `hieVariant` setting.
- this can be overridden by an explicit `hieExecutablePath`, as before.
## 0.0.39
Remove verbose logging option, it is not longer supported.
## 0.0.38
Bump dependencies
## 0.0.37
Trying again, working 0.0.35
- Add Restart command (@gdziadkiewicz)
- Add Ormolu as a formatter option (@DavSanchez)
- Update README
## 0.0.36
- Roll back to 0.0.34
## 0.0.35
- Add Restart command (@gdziadkiewicz)
- Add Ormolu as a formatter option (@DavSanchez)
- Update README
## 0.0.34
- Remove --lsp parameter from hie-vscode.bat
## 0.0.33
- Introduced configuration setting `noLspParam`, default `false` to control
setting the `--lsp` flag for the hie server. So by default we will set the
command line argument for the server, but it can be turned off.
## 0.0.32
- Re-enable the `--lsp` flag for the hie server
- Update some deps for security vulnerabilities
## 0.0.31
- Log to stderr (vscode output) by default, add option for logfile (@bubba)
## 0.0.30
- Bundle using webpack (@chrismwendt)
- Bump protocol version to 3.15 prerelease (@alanz)
This allows working progress reporting from hie.
- Update casesplit plugin (@Avi-D-coder)
## 0.0.29
- bump protocol version to 3.15 (prerelease) (@alanz)
- upgrade deps, including avoiding vulnerabilities on lodash (@alanz)
- warn about compile time and wrapped hie (@janat08)
## 0.0.28
- remove unused `lsp` flag (@bubba)
- do not start `hie` if `hie-wrapper` crashes (@bubba)
- Expose diagnosticsOnChange option for settings (Frederik Ramcke)
- Avoid CVE on `extend` package
- Enable displaying window progress (@bubba)
## 0.0.27
- Re-enable search feature for documentation (@anonimitoraf)
Accesed via `ctrl-f`.
## 0.0.26
- Show documentation content using Webview API (@EdAllonby)
- npm audit fix (@alanz)
## 0.0.25
- Add vsce dependency to "Contributing" document (@EdAllonby)
- Add formatterProvider config (@bubba)
- Bugfix for stack version on windows (@beauzeaux)
- Update settings to match hie version 0.7.0.0 (@alanz)
- npm audit fix (@bubba)
## 0.0.24
- Add snippet config option (@bubba)
## 0.0.23
- Fix multi-process issue, where vscode would launch multiple hie instances.
By @kfigiela
## 0.0.22
- Add configuration option to enable liquid haskell processing. This
is a preview feature of hie from
ca2d3eaa19da8ec9d55521b461d8e2e8cffee697 on 2019-09-05.
## 0.0.21
- Remove languageServerHaskell.useHieWrapper, We now use hie-wrapper
by default.
- Update the vscode-languageclient to v4.4.0
- Fix #98 Import identifier insertion line `moduleLine` is now the
first line that is (trimmed) `where` or ends with `where` or ends
with `)where`. (@mpilgrem)
## 0.0.20
- Add the case-split function (@txsmith). Required hie >= 0.2.1.0
- Update the vscode-languageclient to v4.2.0 (@Bubba)
- Use the hie-wrapper executable now installed with hie to choose the
right version of hie to use for the given project.
## 0.0.19
- Fix hie launch on windows with logging off (#90). Thanks @Tehnix.
## 0.0.18
- Support GHC 8.4.3 in the wrapper file
- The `languageServerHaskell.trace.server` parameter now affects
`/tmp/hie.log`, as well as ghc-mod `--vomit` output.
- Add an Import identifier command, by @chrismwendt
## 0.0.17
- Support GHC 8.4.2 in the wrapper file
- Update dependencies to avoid security vulnerability.
- Use os.tmpdir() for the hie.log file
## 0.0.15
Support the new webview-api for the documentation browser, thanks to @AlexeyRaga.
## 0.0.14
Revert `vscode-languageclient` dependency to version 3.5.0, since version 4.x for some
reason breaks the documentation browser.
## 0.0.13
Add configuration to set the path to your HIE executable, if it's not on your PATH. Note
that this adds the `--lsp` argument to the call of this executable.
## 0.0.12
Add configuration to enable/disable HIE, useful for multi-root workspaces.
## 0.0.11
Add additional marketplace categories.
## 0.0.10
Add support for multi-root workspaces, thanks to @tehnix. See the README section
on [_Using multi-root workspaces_](https://github.com/alanz/vscode-hie-server#using-multi-root-workspaces) for more.
## 0.0.9
Publish to the visual studio marketplace through travis CI via git tags. E.g.
`git tag -a 0.0.9 -m "Version 0.0.9"` and then `git push origin 0.0.9`.
## 0.0.8
Add new haskell-ide-engine logo, thanks to @damienflament
Add rudimentary support for detecting the project GHC version and using the
appropriate hie version. This currently only works on Linux (contributors on
other platforms, please jump in with appropriate scripts) and requires
`haskell-ide-engine` built via the `Makefile` added in
https://github.com/haskell/haskell-ide-engine/pull/447. Thanks to @Tehnix
## 0.0.7
Update `package-lock.json` to fresh dependencies.
Add show type _of selected expression_ on hover feature, by @halhenke
Added options for how to display the same information when using the show type
command menu, by @halhenke
Moved the configuration setting about showing trace information into the proper
scope, by @halhenke
## 0.0.6
Update `package-lock.json` to fresh dependencies.
Update the installation check on Win32 platforms, by @soylens.
Use `tslint` on the plugin sources, by @halhenke.
## 0.0.5
Stop the output channel from taking focus on startup, by @Tehnix and @halhenke
Rework and improve the document layout, for gihub and the marketplace, by @Tehnix
Set up Travis testing an potential auto-deply to marketplace, by @Tehnix
## 0.0.4
Show documents in a tab, by @AlexeyRaga
Add a configuration option to enable/disable `hlint`.
## 0.0.3
Add "Haskell: Show type" command, bound to Ctrl-alt-t (Cmd-alt-t on mac). This
calls the `ghc-mod` `type` command on the current cursor location or highlighted
region. Thanks to @AlexeyRaga
Add a check for having the `hie` executable in the path on startup, to prevent
an endless failure to start if the executable is not there. Thanks to @DavidEichman
## 0.0.2
Add some HaRe commands, accesible via the command palette.
## 0.0.1
Initial release of haskell-ide-engine VS Code extension, for brave pioneers.
================================================
FILE: GenChangelogs.hs
================================================
#!/usr/bin/env cabal
{- cabal:
build-depends: base, bytestring, process, text, github, time >= 1.9
-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
import Control.Monad
import qualified Data.ByteString.Char8 as BS
import Data.List
import Data.Maybe
import qualified Data.Text as T
import Data.Time.Format.ISO8601
import Data.Time.LocalTime
import GitHub
import System.Environment
import System.Process
main = do
callCommand "git fetch --tags"
tag <- last . lines <$>
readProcess "git" ["tag", "--list", "--sort=v:refname"] ""
lastDateStr <- last . lines <$> readProcess "git" ["show", "-s", "--format=%cI", "-1", tag] ""
lastDate <- zonedTimeToUTC <$> iso8601ParseM lastDateStr
args <- getArgs
let githubReq = case args of
[] -> github'
token:_ -> github (OAuth $ BS.pack token)
prs <- githubReq $ pullRequestsForR "haskell" "vscode-haskell" stateClosed FetchAll
let prsAfterLastTag = either (error . show)
(foldMap (\pr -> [pr | inRange pr, isNotDependabot pr]))
prs
inRange pr
| Just mergedDate <- simplePullRequestMergedAt pr = mergedDate > lastDate
| otherwise = False
isNotDependabot SimplePullRequest{..} =
untagName (simpleUserLogin simplePullRequestUser) /= "dependabot[bot]"
forM_ prsAfterLastTag $ \SimplePullRequest{..} ->
putStrLn $ T.unpack $ "- " <> simplePullRequestTitle <> "\n" <>
" ([#" <> T.pack (show $ unIssueNumber simplePullRequestNumber) <> "](" <> getUrl simplePullRequestHtmlUrl <> "))" <>
" by @" <> untagName (simpleUserLogin simplePullRequestUser)
================================================
FILE: LICENSE
================================================
Based on https://github.com/Microsoft/vscode-languageserver-node-example
which has the following license requirement :
-----------------------------------------------------
Copyright (c) Microsoft Corporation
All rights reserved.
MIT License
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:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
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.
================================================
FILE: README.md
================================================
# Haskell for Visual Studio Code
[](https://marketplace.visualstudio.com/items?itemName=haskell.haskell)
This extension adds language support for [Haskell](https://haskell.org), powered by the [Haskell Language Server](https://github.com/haskell/haskell-language-server).
As almost all features are provided by the server you might find interesting read its [documentation](https://haskell-language-server.readthedocs.io).
## Table of Contents
- [Haskell for Visual Studio Code](#haskell-for-visual-studio-code)
- [Table of Contents](#table-of-contents)
- [Setup](#setup)
- [Features](#features)
- [Requirements](#requirements)
- [Configuration options](#configuration-options)
- [Path to server executable](#path-to-server-executable)
- [Security warning](#security-warning)
- [Set additional environment variables for the server](#set-additional-environment-variables-for-the-server)
- [Downloaded binaries](#downloaded-binaries)
- [Setting a specific toolchain](#setting-a-specific-toolchain)
- [Supported GHC versions](#supported-ghc-versions)
- [Using multi-root workspaces](#using-multi-root-workspaces)
- [Investigating and reporting problems](#investigating-and-reporting-problems)
- [FAQ](#faq)
- [Troubleshooting](#troubleshooting)
- [Check issues and tips in the haskell-language-server project](#check-issues-and-tips-in-the-haskell-language-server-project)
- [Restarting the language server](#restarting-the-language-server)
- [`Failed to get project GHC version` on darwin M1 with stack](#failed-to-get-project-ghc-version-on-darwin-m1-with-stack)
- [`GHC ABIs don't match`](#ghc-abis-dont-match)
- [Using an old configuration](#using-an-old-configuration)
- [Stack/Cabal/GHC can not be found](#stackcabalghc-can-not-be-found)
- [Contributing](#contributing)
- [Release Notes](#release-notes)
## Setup
This Extension comes with "batteries"-included and can manage your Haskell Language Server installations for you,
powered by [GHCup](https://www.haskell.org/ghcup/).
Installation of [GHCup](https://www.haskell.org/ghcup/) can not happen automatically, so if you want your HLS installations to be
managed by the Extension, you will have to follow the [installation instructions for GHCup](https://www.haskell.org/ghcup/).
**Note:** Make sure you have a working `ghcup` installation, before launching the Extension.
## Features
You can watch demos for some of these features [here](https://haskell-language-server.readthedocs.io/en/latest/features.html#demos).
- Warning and error diagnostics from GHC
- Type information and documentation on hover
- Jump to definition: [for now only for local code definitions](https://github.com/haskell/haskell-language-server/issues/708)
- Document symbols
- Highlight references in document
- Code completion
- Show documentation and sources in hackage
- 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)
- [Multi-root workspace](https://code.visualstudio.com/docs/editor/multi-root-workspaces) support
- [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)
- [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
- [Code lenses for explicit import lists](https://haskell-language-server.readthedocs.io/en/latest/features.html#explicit-import-lists)
- [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)
- [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)
- [Module name suggestions](https://haskell-language-server.readthedocs.io/en/latest/features.html#module-names) for insertion or correction
- [Call hierarchy support](https://haskell-language-server.readthedocs.io/en/latest/features.html#call-hierarchy)
## Requirements
- 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/).
- 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.
- 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).
- 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).
- Alternatively, you can let the extension manage your entire toolchain automatically (you'll be asked on first startup) via
[ghcup](https://www.haskell.org/ghcup/), which should be pre-installed
## Configuration options
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).
For information on how to set configuration in VSCode, see [here](https://code.visualstudio.com/docs/getstarted/settings).
### Path to server executable
If your server is manually installed and not on your path, you can also manually set the path to the executable.
```json
"haskell.serverExecutablePath": "~/.local/bin/haskell-language-server"
```
There are a few placeholders which will be expanded:
- `~`, `${HOME}` and `${home}` will be expanded into your users' home folder.
- `${workspaceFolder}` and `${workspaceRoot}` will expand into your current project root.
#### Security warning
The option has `machine-overridable` scope so it can be changed per workspace.
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.
See [#387](https://github.com/haskell/vscode-haskell/issues/387) for more details.
### Set additional environment variables for the server
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:
```json
{ "haskell.serverEnvironment": { "XDG_CACHE_HOME": "/path/to/my/cache" } }
```
as the server uses the XDG specification for cache directories.
The environment _only will be visible for the lsp server_, not for other extension tasks like find the server executable.
### Downloaded binaries
This extension will download `haskell-language-server` binaries and the rest of the toolchain if you selected to use GHCup during
first start. Check the `haskell.manageHLS` setting.
It will then download the newest version of haskell-language-server which has support for the required ghc.
That means it could use an older version than the latest one, without the last features and bug fixes.
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.
If you have disk space issues, check `ghcup gc --help`.
You can also instruct the extension to use a different installation directory for the toolchain,
e.g. to not interfere with system GHCup installation. Depending on your platform, add the full
resolved path like so:
```json
"haskell.serverEnvironment": {
"GHCUP_INSTALL_BASE_PREFIX": "/home/foo/.config/Code/User/globalStorage/haskell.haskell/"
}
```
The internal storage paths for the extension depend on the platform:
| Platform | Path |
| -------- | ------------------------------------------------------------------------------- |
| macOS | `~/Library/Application\ Support/Code/User/globalStorage/haskell.haskell/.ghcup` |
| Windows | `%APPDATA%\Code\User\globalStorage\haskell.haskell\ghcup` |
| Linux | `$HOME/.config/Code/User/globalStorage/haskell.haskell/.ghcup` |
If you want to manage HLS yourself, set `haskell.manageHLS` to `PATH` and make sure HLS is in your PATH
or set `haskell.serverExecutablePath` (overrides all other settings) to a valid executable.
If you need to set mirrors for ghcup download info, check the settings `haskell.metadataURL` and `haskell.releasesURL`.
### Setting a specific toolchain
When `manageHLS` is set to `GHCup`, you can define a specific toolchain (`hls`, `ghc`, `cabal` and `stack`),
either globally or per project. E.g.:
```json
{
"haskell.toolchain": {
"hls": "1.6.1.1",
"cabal": "recommended",
"stack": null
}
}
```
This means:
1. install the `ghc` version corresponding to the project (default, because it's omitted)
2. install `hls` 1.6.1.1
3. install the recommended `cabal` version from ghcup
4. don't install any `stack` version
Another config could be:
```json
{
"haskell.toolchain": {
"ghc": "9.2.2",
"hls": "latest",
"cabal": "recommended"
}
}
```
Meaning:
1. install `ghc` 9.2.2 regardless of what the project requires
2. always install latest `hls`, even if it doesn't support the given GHC version
3. install recommended `cabal`
4. install latest `stack` (default, because it's omitted)
The defaults (when omitted) are as follows:
1. install the project required `ghc` (corresponding to `with-compiler` setting in `cabal.project` for example)
2. install the latest `hls` version that supports the project required ghc version
3. install latest `cabal`
4. install latest `stack`
When a the value is `null`, the extension will refrain from installing it.
At last, if you don't want `ghcup` to manage any of the external tools except `hls`, you can use:
```json
{
"haskell.toolchain": {
"ghc": null,
"cabal": null,
"stack": null
}
}
```
### Supported GHC versions
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).
[Building from source](https://haskell-language-server.readthedocs.io/en/latest/installation.html) may support more versions!
The exact list of binaries can be checked in the last release of haskell-language-server: <https://github.com/haskell/haskell-language-server/releases/latest>
## Using multi-root workspaces
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.
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.
## Investigating and reporting problems
1. Go to extensions and right click `Haskell` and choose `Extensions Settings`
2. Scroll down to `Haskell › Trace: Server` and set it to `messages`.
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.
4. Restart vscode and reproduce your problem
5. Go to the main menu and choose `View -> Output` (`Ctrl + Shift + U`)
6. On the new Output panel that opens on the right side in the drop down menu choose `Haskell (<your project>)`
Please include the output when filing any issues on the [haskell-language-server](https://github.com/haskell/haskell-language-server/issues/new) issue tracker.
## FAQ
### Troubleshooting
#### Check issues and tips in the haskell-language-server project
- 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.
- You can also check the [troubleshooting section](https://haskell-language-server.readthedocs.io/en/latest/troubleshooting.html) in the server documentation.
#### Restarting the language server
- Sometimes the language server might get stuck in a rut and stop responding to your latest changes.
Should this occur you can try restarting the language server with <kbd>Ctrl</kbd> <kbd>shift</kbd> <kbd>P</kbd>/<kbd>⌘</kbd> <kbd>shift</kbd> <kbd>P</kbd> > Restart Haskell LSP Server.
#### `Failed to get project GHC version` on darwin M1 with stack
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
#### `GHC ABIs don't match`
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).
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):
```yml
setup-info:
ghc:
linux64-tinfo6:
9.0.2:
url: 'https://downloads.haskell.org/ghc/9.0.2/ghc-9.0.2a-x86_64-fedora27-linux.tar.xz'
```
Now make sure to remove cached/installed libraries to avoid getting segfaults at runtime.
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.
#### `hGetContents: invalid argument (invalid byte sequence)`
This problem was encountered on darwin M2 with ghcup.
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)`.
If this is the case, setting `terminal.integrated.detectLocale` to `off` might resolve your issue.
#### Using an old configuration
If something just doesn't work, but you recall an old configuration that did, you
may try forcing a particular setting, e.g. by disabling all automatic installations
except HLS:
```json
"haskell.toolchain": {
"hls": "1.6.1.0",
"ghc": null,
"cabal": null,
"stack": null
}
```
#### Stack/Cabal/GHC can not be found
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:
```json
"haskell.serverEnvironment": {
"PATH": "${HOME}/.ghcup/bin:$PATH"
}
```
## Contributing
If you want to help, get started by reading [Contributing](./docs/Contributing.md) for more details.
## Release Notes
See the [Changelog](./Changelog.md) for more details.
================================================
FILE: docs/Contributing.md
================================================
# Contributing
## Dependencies and Building
Run `npm install` in the project root to install the development dependencies.
You can also package up the extension with
- `npm install` to install the dependencies
- `npm run package` which creates an extension package at `haskell-<version>.vsix`.
_Note:_ that if you get errors running `vsce package`, it might help running `npm run pretest` directly, since that gives the actual error output of the TypeScript compilation.
## Developing inside VS Code
- Launch VS Code, press `File` > `Open Folder`, open the `vscode-haskell` folder;
- press `F5` to open a new window with the `vscode-haskell` loaded (this will overwrite existing ones, e.g. from the marketplace);
- open a Haskell file with the **new** editor to test the LSP client;
You are now ready to make changes and debug. You can,
- set breakpoints in your code inside `src/extension.ts` to debug your extension;
- find output from your extension in the debug console;
- make changes to the code, and then
- relaunch the extension from the debug toolbar
_Note_: you can also reload (`Ctrl+R` or `Cmd+R` on macOS) the VS Code window with your extension to load your changes
#### Formatting
[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.
The configurations for prettier are located in `.prettierrc`.
## Navigating the Files
A brief overview of the files,
- `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
- `src/extension.ts` is the main entrypoint to the extension, and handles launching the language server.
- `src/hlsBinaries.ts` handles automatically installing the pre-built `haskell-language-server` binaries
- `src/utils.ts` has some functions for downloading files and checking if executables are on the path
- `src/docsBrowser.ts` contains the logic for displaying the documentation browser (e.g. hover over a type like `mapM_` and click `Documentation` or `Source`)
## Helpful Reading Material
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.
================================================
FILE: docs/Release.md
================================================
# Release Checklist
Follow this list for items that must be completed for release of the `vscode-haskell` extension.
- [ ] Run `npm audit` for security vulnerabilities.
- Fix vulnerabilities.
- [ ] Run `npm outdated` to find outdated package version, review what needs to be updated.
- [ ] 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.
- [ ] SemVer Compatible Version Bump in `package.json`
- 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.
- [ ] `git switch -c release-<version>`
- [ ] Update ChangeLog.md. The output of `./GenChangelogs.hs` usually suffices.
- [ ] Update the README.md to have no outdated information.
- [ ] Make sure CI is succeeding.
- [ ] Perform the release by creating a [release in Github](https://github.com/haskell/vscode-haskell/releases)
- Github actions will automatically release the extension to VSCode- and VSX-Marketplace.
- 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.
## Branching policy
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.
To simplify the release management, the following policy is in place:
- The branch `master` contains the current pre-release
- As such, its `package.json` must always have the form `major.ODD_NUMBER.patch`
- Dependency version bumps are automatically performed by dependabot against `master`
- For each release, a tag must be created
- Stable releases are located on a separate branch called `release-<major.EVEN_NUMBER>`
- Before a release, the branch is rebased on top of current master
- For each stable release, a tag must be created of the form `major.EVEN_NUMBER.patch`
## Release CI
The release CI has access tokens for VSX Marketplace and the VSCode Marketplace.
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.
The latter needs to be refreshed once a year.
- Send an email to `committee@haskell.org` requesting the token
- Include your public GPG key so they can send you the token encrypted
- Update the repository secrets
- 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
Last time the VSCode Marketplace token was updated: 2023-08-17
================================================
FILE: eslint.config.mjs
================================================
import globals from 'globals';
import eslint from '@eslint/js';
import { defineConfig } from 'eslint/config';
import tseslint from 'typescript-eslint';
export default defineConfig(
{ files: ['**/*.{js,mjs,cjs,ts}'] },
{
languageOptions: {
globals: globals.node,
parserOptions: { projectService: true, tsconfigRootDir: import.meta.dirname },
},
},
{
// disables type checking for this file only
files: ['eslint.config.mjs'],
extends: [tseslint.configs.disableTypeChecked],
},
eslint.configs.recommended,
tseslint.configs.recommendedTypeChecked,
{
rules: {
// turn off these lints as we access workspaceConfiguration fields.
// So far, there was no bug found with these unsafe accesses.
'@typescript-eslint/no-unsafe-assignment': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off',
// Sometimes, the 'any' just saves too much time.
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'error',
'@typescript-eslint/no-unused-vars': [
'error',
{
args: 'all',
argsIgnorePattern: '^_',
caughtErrors: 'all',
caughtErrorsIgnorePattern: '^_',
destructuredArrayIgnorePattern: '^_',
varsIgnorePattern: '^_',
ignoreRestSiblings: true,
},
],
},
},
);
================================================
FILE: flake.nix
================================================
{
description = "VS Code Haskell extension development environment";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
# flake-utils for cross-platform support
flake-utils.url = "github:numtide/flake-utils";
};
# Outputs define what our flake produces
outputs =
{
self,
nixpkgs,
flake-utils,
}:
# This function creates outputs for each system (x86_64-linux, aarch64-darwin, etc.)
flake-utils.lib.eachDefaultSystem (
system:
let
# Import nixpkgs for our specific system
pkgs = nixpkgs.legacyPackages.${system};
in
{
# Development shell with Node.js and VS Code extension tools
devShells.default = pkgs.mkShell {
name = "vscode-haskell-dev";
packages = with pkgs; [
# Node.js runtime (LTS version)
nodejs_20
# Package managers
corepack # Enables yarn/npm/pnpm via packageManager field
# VS Code extension development
vscode # For testing extension
vsce # VS Code Extension CLI (publish/package)
# Additional tools
git
nixpkgs-fmt
];
shellHook = ''
echo "VS Code Haskell Extension Dev Environment"
echo "Node: $(node --version)"
echo "npm: $(npm --version 2>/dev/null || echo 'not active')"
echo ""
echo "Available commands:"
echo " npm install - Install dependencies"
echo " npm build - Build extension with webpack"
echo " npm run watch - Watch mode"
echo " vsce package - Create .vsix package"
echo " vsce publish - Publish to marketplace"
'';
};
}
);
}
================================================
FILE: package.json
================================================
{
"name": "haskell",
"displayName": "Haskell",
"description": "Haskell language support powered by the Haskell Language Server",
"version": "2.8.0",
"license": "MIT",
"publisher": "haskell",
"engines": {
"vscode": "^1.102.0"
},
"keywords": [
"language",
"haskell",
"cabal",
"stack",
"lsp",
"multi-root ready"
],
"homepage": "https://github.com/haskell/vscode-haskell",
"repository": "https://github.com/haskell/vscode-haskell.git",
"bugs": {
"url": "https://github.com/haskell/vscode-haskell/issues"
},
"categories": [
"Programming Languages",
"Formatters",
"Linters",
"Other"
],
"icon": "images/hls-logo.png",
"galleryBanner": {
"color": "#22172A",
"theme": "dark"
},
"activationEvents": [
"onLanguage:haskell",
"onLanguage:literate haskell",
"onLanguage:cabal"
],
"main": "./dist/extension",
"contributes": {
"languages": [
{
"id": "haskell",
"aliases": [
"Haskell",
"haskell"
],
"extensions": [
".hs"
]
},
{
"id": "cabal",
"aliases": [
"Cabal"
],
"extensions": [
".cabal"
]
},
{
"id": "literate haskell",
"aliases": [
"Literate Haskell",
"literate Haskell"
],
"extensions": [
".lhs"
]
}
],
"configuration": {
"type": "object",
"title": "Haskell",
"properties": {
"haskell.formattingProvider": {
"scope": "resource",
"type": "string",
"enum": [
"brittany",
"floskell",
"fourmolu",
"ormolu",
"stylish-haskell",
"none"
],
"default": "ormolu",
"description": "The formatter to use when formatting a document or range. Ensure the plugin is enabled."
},
"haskell.cabalFormattingProvider": {
"scope": "resource",
"type": "string",
"enum": [
"cabal-gild",
"cabal-fmt",
"none"
],
"default": "cabal-gild",
"description": "The formatter to use when formatting a document or range of a cabal formatter. Ensure the plugin is enabled."
},
"haskell.openDocumentationInHackage": {
"scope": "resource",
"type": "boolean",
"default": true,
"description": "When opening 'Documentation' for external libraries, open in hackage by default. Set to false to instead open in vscode."
},
"haskell.openSourceInHackage": {
"scope": "resource",
"type": "boolean",
"default": true,
"description": "When opening 'Source' for external libraries, open in hackage by default. Set to false to instead open in vscode."
},
"haskell.trace.server": {
"scope": "resource",
"type": "string",
"enum": [
"off",
"messages",
"verbose"
],
"default": "off",
"description": "Traces the communication between VS Code and the language server."
},
"haskell.trace.client": {
"scope": "resource",
"type": "string",
"enum": [
"off",
"error",
"info",
"debug"
],
"default": "info",
"description": "Sets the log level in the client side."
},
"haskell.logFile": {
"scope": "resource",
"type": "string",
"default": "",
"description": "If set, redirects the logs to a file."
},
"haskell.releasesURL": {
"scope": "resource",
"type": "string",
"default": "",
"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)"
},
"haskell.metadataURL": {
"scope": "resource",
"type": "string",
"default": "",
"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)"
},
"haskell.releasesDownloadStoragePath": {
"scope": "resource",
"type": "string",
"default": "",
"markdownDescription": "An optional path where downloaded metadata will be stored. Check the default value [here](https://github.com/haskell/vscode-haskell#downloaded-binaries)"
},
"haskell.serverExecutablePath": {
"scope": "machine-overridable",
"type": "string",
"default": "",
"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}`."
},
"haskell.serverExtraArgs": {
"scope": "resource",
"type": "string",
"default": "",
"markdownDescription": "Pass additional arguments to the language server."
},
"haskell.ghcupExecutablePath": {
"scope": "resource",
"type": "string",
"default": "",
"markdownDescription": "Manually set a ghcup executable path."
},
"haskell.serverEnvironment": {
"scope": "resource",
"type": "object",
"default": {},
"markdownDescription": "Define environment variables for the language server."
},
"haskell.promptBeforeDownloads": {
"scope": "machine",
"type": "boolean",
"default": "true",
"markdownDescription": "Prompt before performing any downloads."
},
"haskell.manageHLS": {
"scope": "resource",
"type": "string",
"default": "PATH",
"description": "How to manage/find HLS installations.",
"enum": [
"GHCup",
"PATH"
],
"enumDescriptions": [
"Will use ghcup and manage Haskell toolchain in the default location (usually '~/.ghcup')",
"Discovers HLS and other executables in system PATH"
]
},
"haskell.toolchain": {
"scope": "resource",
"type": "object",
"default": {},
"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."
},
"haskell.upgradeGHCup": {
"scope": "resource",
"type": "boolean",
"default": true,
"description": "Whether to upgrade GHCup automatically when 'manageHLS' is set to 'GHCup'."
},
"haskell.checkProject": {
"scope": "resource",
"type": "boolean",
"default": true,
"description": "Whether to typecheck the entire project on load. It could drive to bad performance in large projects."
},
"haskell.sessionLoading": {
"scope": "resource",
"type": "string",
"enum": [
"singleComponent",
"multipleComponents"
],
"default": "singleComponent",
"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.",
"enumDescriptions": [
"Always load only a single component at a time. This is the most reliable option if you encountered any issues with the other options.",
"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."
]
},
"haskell.supportCabalFiles": {
"scope": "resource",
"default": "automatic",
"type": "string",
"enum": [
"enable",
"disable",
"automatic"
],
"description": "Enable Language Server support for `.cabal` files. Requires Haskell Language Server version >= 1.9.0.0.",
"enumDescriptions": [
"Enable Language Server support for `.cabal` files",
"Disable Language Server support for `.cabal` files",
"Enable Language Server support for `.cabal` files if the HLS version supports it."
]
},
"haskell.maxCompletions": {
"scope": "resource",
"default": 40,
"type": "integer",
"description": "Maximum number of completions sent to the editor."
},
"haskell.plugin.alternateNumberFormat.globalOn": {
"default": true,
"description": "Enables alternateNumberFormat plugin",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.cabal-fmt.config.path": {
"default": "cabal-fmt",
"markdownDescription": "Set path to 'cabal-fmt' executable",
"scope": "resource",
"type": "string"
},
"haskell.plugin.cabal-gild.config.path": {
"default": "cabal-gild",
"markdownDescription": "Set path to 'cabal-gild' executable",
"scope": "resource",
"type": "string"
},
"haskell.plugin.cabal.codeActionsOn": {
"default": true,
"description": "Enables cabal code actions",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.cabal.completionOn": {
"default": true,
"description": "Enables cabal completions",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.cabal.diagnosticsOn": {
"default": true,
"description": "Enables cabal diagnostics",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.cabal.hoverOn": {
"default": true,
"description": "Enables cabal hover",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.cabal.symbolsOn": {
"default": true,
"description": "Enables cabal symbols",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.cabalHaskellIntegration.globalOn": {
"default": true,
"description": "Enables cabalHaskellIntegration plugin",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.callHierarchy.globalOn": {
"default": true,
"description": "Enables callHierarchy plugin",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.changeTypeSignature.globalOn": {
"default": true,
"description": "Enables changeTypeSignature plugin",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.class.codeActionsOn": {
"default": true,
"description": "Enables class code actions",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.class.codeLensOn": {
"default": true,
"description": "Enables class code lenses",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.eval.codeActionsOn": {
"default": true,
"description": "Enables eval code actions",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.eval.codeLensOn": {
"default": true,
"description": "Enables eval code lenses",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.eval.config.diff": {
"default": true,
"markdownDescription": "Enable the diff output (WAS/NOW) of eval lenses",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.eval.config.exception": {
"default": false,
"markdownDescription": "Enable marking exceptions with `*** Exception:` similarly to doctest and GHCi.",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.explicit-fields.codeActionsOn": {
"default": true,
"description": "Enables explicit-fields code actions",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.explicit-fields.inlayHintsOn": {
"default": true,
"description": "Enables explicit-fields inlay hints",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.explicit-fixity.globalOn": {
"default": true,
"description": "Enables explicit-fixity plugin",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.fourmolu.config.external": {
"default": false,
"markdownDescription": "Call out to an external \"fourmolu\" executable, rather than using the bundled library.",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.fourmolu.config.path": {
"default": "fourmolu",
"markdownDescription": "Set path to executable (for \"external\" mode).",
"scope": "resource",
"type": "string"
},
"haskell.plugin.gadt.globalOn": {
"default": true,
"description": "Enables gadt plugin",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.ghcide-code-actions-bindings.globalOn": {
"default": true,
"description": "Enables ghcide-code-actions-bindings plugin",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.ghcide-code-actions-fill-holes.globalOn": {
"default": true,
"description": "Enables ghcide-code-actions-fill-holes plugin",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.ghcide-code-actions-imports-exports.globalOn": {
"default": true,
"description": "Enables ghcide-code-actions-imports-exports plugin",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.ghcide-code-actions-type-signatures.globalOn": {
"default": true,
"description": "Enables ghcide-code-actions-type-signatures plugin",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.ghcide-completions.config.autoExtendOn": {
"default": true,
"markdownDescription": "Extends the import list automatically when completing a out-of-scope identifier",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.ghcide-completions.config.snippetsOn": {
"default": true,
"markdownDescription": "Inserts snippets when using code completions",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.ghcide-completions.globalOn": {
"default": true,
"description": "Enables ghcide-completions plugin",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.ghcide-hover-and-symbols.hoverOn": {
"default": true,
"description": "Enables ghcide-hover-and-symbols hover",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.ghcide-hover-and-symbols.symbolsOn": {
"default": true,
"description": "Enables ghcide-hover-and-symbols symbols",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.ghcide-type-lenses.config.mode": {
"default": "always",
"description": "Control how type lenses are shown",
"enum": [
"always",
"exported",
"diagnostics"
],
"enumDescriptions": [
"Always displays type lenses of global bindings",
"Only display type lenses of exported global bindings",
"Follows error messages produced by GHC about missing signatures"
],
"scope": "resource",
"type": "string"
},
"haskell.plugin.ghcide-type-lenses.globalOn": {
"default": true,
"description": "Enables ghcide-type-lenses plugin",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.hlint.codeActionsOn": {
"default": true,
"description": "Enables hlint code actions",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.hlint.config.flags": {
"default": [],
"markdownDescription": "Flags used by hlint",
"scope": "resource",
"type": "array"
},
"haskell.plugin.hlint.diagnosticsOn": {
"default": true,
"description": "Enables hlint diagnostics",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.importLens.codeActionsOn": {
"default": true,
"description": "Enables importLens code actions",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.importLens.codeLensOn": {
"default": true,
"description": "Enables importLens code lenses",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.importLens.inlayHintsOn": {
"default": true,
"description": "Enables importLens inlay hints",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.moduleName.globalOn": {
"default": true,
"description": "Enables moduleName plugin",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.ormolu.config.external": {
"default": false,
"markdownDescription": "Call out to an external \"ormolu\" executable, rather than using the bundled library",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.overloaded-record-dot.globalOn": {
"default": true,
"description": "Enables overloaded-record-dot plugin",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.pragmas-completion.globalOn": {
"default": true,
"description": "Enables pragmas-completion plugin",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.pragmas-disable.globalOn": {
"default": true,
"description": "Enables pragmas-disable plugin",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.pragmas-suggest.globalOn": {
"default": true,
"description": "Enables pragmas-suggest plugin",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.qualifyImportedNames.globalOn": {
"default": true,
"description": "Enables qualifyImportedNames plugin",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.rename.config.crossModule": {
"default": false,
"markdownDescription": "Enable experimental cross-module renaming",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.rename.globalOn": {
"default": true,
"description": "Enables rename plugin",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.retrie.globalOn": {
"default": true,
"description": "Enables retrie plugin",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.semanticTokens.config.classMethodToken": {
"default": "method",
"description": "LSP semantic token type to use for typeclass methods",
"enum": [
"namespace",
"type",
"class",
"enum",
"interface",
"struct",
"typeParameter",
"parameter",
"variable",
"property",
"enumMember",
"event",
"function",
"method",
"macro",
"keyword",
"modifier",
"comment",
"string",
"number",
"regexp",
"operator",
"decorator"
],
"enumDescriptions": [
"LSP Semantic Token Type: namespace",
"LSP Semantic Token Type: type",
"LSP Semantic Token Type: class",
"LSP Semantic Token Type: enum",
"LSP Semantic Token Type: interface",
"LSP Semantic Token Type: struct",
"LSP Semantic Token Type: typeParameter",
"LSP Semantic Token Type: parameter",
"LSP Semantic Token Type: variable",
"LSP Semantic Token Type: property",
"LSP Semantic Token Type: enumMember",
"LSP Semantic Token Type: event",
"LSP Semantic Token Type: function",
"LSP Semantic Token Type: method",
"LSP Semantic Token Type: macro",
"LSP Semantic Token Type: keyword",
"LSP Semantic Token Type: modifier",
"LSP Semantic Token Type: comment",
"LSP Semantic Token Type: string",
"LSP Semantic Token Type: number",
"LSP Semantic Token Type: regexp",
"LSP Semantic Token Type: operator",
"LSP Semantic Token Type: decorator"
],
"scope": "resource",
"type": "string"
},
"haskell.plugin.semanticTokens.config.classToken": {
"default": "class",
"description": "LSP semantic token type to use for typeclasses",
"enum": [
"namespace",
"type",
"class",
"enum",
"interface",
"struct",
"typeParameter",
"parameter",
"variable",
"property",
"enumMember",
"event",
"function",
"method",
"macro",
"keyword",
"modifier",
"comment",
"string",
"number",
"regexp",
"operator",
"decorator"
],
"enumDescriptions": [
"LSP Semantic Token Type: namespace",
"LSP Semantic Token Type: type",
"LSP Semantic Token Type: class",
"LSP Semantic Token Type: enum",
"LSP Semantic Token Type: interface",
"LSP Semantic Token Type: struct",
"LSP Semantic Token Type: typeParameter",
"LSP Semantic Token Type: parameter",
"LSP Semantic Token Type: variable",
"LSP Semantic Token Type: property",
"LSP Semantic Token Type: enumMember",
"LSP Semantic Token Type: event",
"LSP Semantic Token Type: function",
"LSP Semantic Token Type: method",
"LSP Semantic Token Type: macro",
"LSP Semantic Token Type: keyword",
"LSP Semantic Token Type: modifier",
"LSP Semantic Token Type: comment",
"LSP Semantic Token Type: string",
"LSP Semantic Token Type: number",
"LSP Semantic Token Type: regexp",
"LSP Semantic Token Type: operator",
"LSP Semantic Token Type: decorator"
],
"scope": "resource",
"type": "string"
},
"haskell.plugin.semanticTokens.config.dataConstructorToken": {
"default": "enumMember",
"description": "LSP semantic token type to use for data constructors",
"enum": [
"namespace",
"type",
"class",
"enum",
"interface",
"struct",
"typeParameter",
"parameter",
"variable",
"property",
"enumMember",
"event",
"function",
"method",
"macro",
"keyword",
"modifier",
"comment",
"string",
"number",
"regexp",
"operator",
"decorator"
],
"enumDescriptions": [
"LSP Semantic Token Type: namespace",
"LSP Semantic Token Type: type",
"LSP Semantic Token Type: class",
"LSP Semantic Token Type: enum",
"LSP Semantic Token Type: interface",
"LSP Semantic Token Type: struct",
"LSP Semantic Token Type: typeParameter",
"LSP Semantic Token Type: parameter",
"LSP Semantic Token Type: variable",
"LSP Semantic Token Type: property",
"LSP Semantic Token Type: enumMember",
"LSP Semantic Token Type: event",
"LSP Semantic Token Type: function",
"LSP Semantic Token Type: method",
"LSP Semantic Token Type: macro",
"LSP Semantic Token Type: keyword",
"LSP Semantic Token Type: modifier",
"LSP Semantic Token Type: comment",
"LSP Semantic Token Type: string",
"LSP Semantic Token Type: number",
"LSP Semantic Token Type: regexp",
"LSP Semantic Token Type: operator",
"LSP Semantic Token Type: decorator"
],
"scope": "resource",
"type": "string"
},
"haskell.plugin.semanticTokens.config.functionToken": {
"default": "function",
"description": "LSP semantic token type to use for functions",
"enum": [
"namespace",
"type",
"class",
"enum",
"interface",
"struct",
"typeParameter",
"parameter",
"variable",
"property",
"enumMember",
"event",
"function",
"method",
"macro",
"keyword",
"modifier",
"comment",
"string",
"number",
"regexp",
"operator",
"decorator"
],
"enumDescriptions": [
"LSP Semantic Token Type: namespace",
"LSP Semantic Token Type: type",
"LSP Semantic Token Type: class",
"LSP Semantic Token Type: enum",
"LSP Semantic Token Type: interface",
"LSP Semantic Token Type: struct",
"LSP Semantic Token Type: typeParameter",
"LSP Semantic Token Type: parameter",
"LSP Semantic Token Type: variable",
"LSP Semantic Token Type: property",
"LSP Semantic Token Type: enumMember",
"LSP Semantic Token Type: event",
"LSP Semantic Token Type: function",
"LSP Semantic Token Type: method",
"LSP Semantic Token Type: macro",
"LSP Semantic Token Type: keyword",
"LSP Semantic Token Type: modifier",
"LSP Semantic Token Type: comment",
"LSP Semantic Token Type: string",
"LSP Semantic Token Type: number",
"LSP Semantic Token Type: regexp",
"LSP Semantic Token Type: operator",
"LSP Semantic Token Type: decorator"
],
"scope": "resource",
"type": "string"
},
"haskell.plugin.semanticTokens.config.moduleToken": {
"default": "namespace",
"description": "LSP semantic token type to use for modules",
"enum": [
"namespace",
"type",
"class",
"enum",
"interface",
"struct",
"typeParameter",
"parameter",
"variable",
"property",
"enumMember",
"event",
"function",
"method",
"macro",
"keyword",
"modifier",
"comment",
"string",
"number",
"regexp",
"operator",
"decorator"
],
"enumDescriptions": [
"LSP Semantic Token Type: namespace",
"LSP Semantic Token Type: type",
"LSP Semantic Token Type: class",
"LSP Semantic Token Type: enum",
"LSP Semantic Token Type: interface",
"LSP Semantic Token Type: struct",
"LSP Semantic Token Type: typeParameter",
"LSP Semantic Token Type: parameter",
"LSP Semantic Token Type: variable",
"LSP Semantic Token Type: property",
"LSP Semantic Token Type: enumMember",
"LSP Semantic Token Type: event",
"LSP Semantic Token Type: function",
"LSP Semantic Token Type: method",
"LSP Semantic Token Type: macro",
"LSP Semantic Token Type: keyword",
"LSP Semantic Token Type: modifier",
"LSP Semantic Token Type: comment",
"LSP Semantic Token Type: string",
"LSP Semantic Token Type: number",
"LSP Semantic Token Type: regexp",
"LSP Semantic Token Type: operator",
"LSP Semantic Token Type: decorator"
],
"scope": "resource",
"type": "string"
},
"haskell.plugin.semanticTokens.config.operatorToken": {
"default": "operator",
"description": "LSP semantic token type to use for operators",
"enum": [
"namespace",
"type",
"class",
"enum",
"interface",
"struct",
"typeParameter",
"parameter",
"variable",
"property",
"enumMember",
"event",
"function",
"method",
"macro",
"keyword",
"modifier",
"comment",
"string",
"number",
"regexp",
"operator",
"decorator"
],
"enumDescriptions": [
"LSP Semantic Token Type: namespace",
"LSP Semantic Token Type: type",
"LSP Semantic Token Type: class",
"LSP Semantic Token Type: enum",
"LSP Semantic Token Type: interface",
"LSP Semantic Token Type: struct",
"LSP Semantic Token Type: typeParameter",
"LSP Semantic Token Type: parameter",
"LSP Semantic Token Type: variable",
"LSP Semantic Token Type: property",
"LSP Semantic Token Type: enumMember",
"LSP Semantic Token Type: event",
"LSP Semantic Token Type: function",
"LSP Semantic Token Type: method",
"LSP Semantic Token Type: macro",
"LSP Semantic Token Type: keyword",
"LSP Semantic Token Type: modifier",
"LSP Semantic Token Type: comment",
"LSP Semantic Token Type: string",
"LSP Semantic Token Type: number",
"LSP Semantic Token Type: regexp",
"LSP Semantic Token Type: operator",
"LSP Semantic Token Type: decorator"
],
"scope": "resource",
"type": "string"
},
"haskell.plugin.semanticTokens.config.patternSynonymToken": {
"default": "macro",
"description": "LSP semantic token type to use for pattern synonyms",
"enum": [
"namespace",
"type",
"class",
"enum",
"interface",
"struct",
"typeParameter",
"parameter",
"variable",
"property",
"enumMember",
"event",
"function",
"method",
"macro",
"keyword",
"modifier",
"comment",
"string",
"number",
"regexp",
"operator",
"decorator"
],
"enumDescriptions": [
"LSP Semantic Token Type: namespace",
"LSP Semantic Token Type: type",
"LSP Semantic Token Type: class",
"LSP Semantic Token Type: enum",
"LSP Semantic Token Type: interface",
"LSP Semantic Token Type: struct",
"LSP Semantic Token Type: typeParameter",
"LSP Semantic Token Type: parameter",
"LSP Semantic Token Type: variable",
"LSP Semantic Token Type: property",
"LSP Semantic Token Type: enumMember",
"LSP Semantic Token Type: event",
"LSP Semantic Token Type: function",
"LSP Semantic Token Type: method",
"LSP Semantic Token Type: macro",
"LSP Semantic Token Type: keyword",
"LSP Semantic Token Type: modifier",
"LSP Semantic Token Type: comment",
"LSP Semantic Token Type: string",
"LSP Semantic Token Type: number",
"LSP Semantic Token Type: regexp",
"LSP Semantic Token Type: operator",
"LSP Semantic Token Type: decorator"
],
"scope": "resource",
"type": "string"
},
"haskell.plugin.semanticTokens.config.recordFieldToken": {
"default": "property",
"description": "LSP semantic token type to use for record fields",
"enum": [
"namespace",
"type",
"class",
"enum",
"interface",
"struct",
"typeParameter",
"parameter",
"variable",
"property",
"enumMember",
"event",
"function",
"method",
"macro",
"keyword",
"modifier",
"comment",
"string",
"number",
"regexp",
"operator",
"decorator"
],
"enumDescriptions": [
"LSP Semantic Token Type: namespace",
"LSP Semantic Token Type: type",
"LSP Semantic Token Type: class",
"LSP Semantic Token Type: enum",
"LSP Semantic Token Type: interface",
"LSP Semantic Token Type: struct",
"LSP Semantic Token Type: typeParameter",
"LSP Semantic Token Type: parameter",
"LSP Semantic Token Type: variable",
"LSP Semantic Token Type: property",
"LSP Semantic Token Type: enumMember",
"LSP Semantic Token Type: event",
"LSP Semantic Token Type: function",
"LSP Semantic Token Type: method",
"LSP Semantic Token Type: macro",
"LSP Semantic Token Type: keyword",
"LSP Semantic Token Type: modifier",
"LSP Semantic Token Type: comment",
"LSP Semantic Token Type: string",
"LSP Semantic Token Type: number",
"LSP Semantic Token Type: regexp",
"LSP Semantic Token Type: operator",
"LSP Semantic Token Type: decorator"
],
"scope": "resource",
"type": "string"
},
"haskell.plugin.semanticTokens.config.typeConstructorToken": {
"default": "enum",
"description": "LSP semantic token type to use for type constructors",
"enum": [
"namespace",
"type",
"class",
"enum",
"interface",
"struct",
"typeParameter",
"parameter",
"variable",
"property",
"enumMember",
"event",
"function",
"method",
"macro",
"keyword",
"modifier",
"comment",
"string",
"number",
"regexp",
"operator",
"decorator"
],
"enumDescriptions": [
"LSP Semantic Token Type: namespace",
"LSP Semantic Token Type: type",
"LSP Semantic Token Type: class",
"LSP Semantic Token Type: enum",
"LSP Semantic Token Type: interface",
"LSP Semantic Token Type: struct",
"LSP Semantic Token Type: typeParameter",
"LSP Semantic Token Type: parameter",
"LSP Semantic Token Type: variable",
"LSP Semantic Token Type: property",
"LSP Semantic Token Type: enumMember",
"LSP Semantic Token Type: event",
"LSP Semantic Token Type: function",
"LSP Semantic Token Type: method",
"LSP Semantic Token Type: macro",
"LSP Semantic Token Type: keyword",
"LSP Semantic Token Type: modifier",
"LSP Semantic Token Type: comment",
"LSP Semantic Token Type: string",
"LSP Semantic Token Type: number",
"LSP Semantic Token Type: regexp",
"LSP Semantic Token Type: operator",
"LSP Semantic Token Type: decorator"
],
"scope": "resource",
"type": "string"
},
"haskell.plugin.semanticTokens.config.typeFamilyToken": {
"default": "interface",
"description": "LSP semantic token type to use for type families",
"enum": [
"namespace",
"type",
"class",
"enum",
"interface",
"struct",
"typeParameter",
"parameter",
"variable",
"property",
"enumMember",
"event",
"function",
"method",
"macro",
"keyword",
"modifier",
"comment",
"string",
"number",
"regexp",
"operator",
"decorator"
],
"enumDescriptions": [
"LSP Semantic Token Type: namespace",
"LSP Semantic Token Type: type",
"LSP Semantic Token Type: class",
"LSP Semantic Token Type: enum",
"LSP Semantic Token Type: interface",
"LSP Semantic Token Type: struct",
"LSP Semantic Token Type: typeParameter",
"LSP Semantic Token Type: parameter",
"LSP Semantic Token Type: variable",
"LSP Semantic Token Type: property",
"LSP Semantic Token Type: enumMember",
"LSP Semantic Token Type: event",
"LSP Semantic Token Type: function",
"LSP Semantic Token Type: method",
"LSP Semantic Token Type: macro",
"LSP Semantic Token Type: keyword",
"LSP Semantic Token Type: modifier",
"LSP Semantic Token Type: comment",
"LSP Semantic Token Type: string",
"LSP Semantic Token Type: number",
"LSP Semantic Token Type: regexp",
"LSP Semantic Token Type: operator",
"LSP Semantic Token Type: decorator"
],
"scope": "resource",
"type": "string"
},
"haskell.plugin.semanticTokens.config.typeSynonymToken": {
"default": "type",
"description": "LSP semantic token type to use for type synonyms",
"enum": [
"namespace",
"type",
"class",
"enum",
"interface",
"struct",
"typeParameter",
"parameter",
"variable",
"property",
"enumMember",
"event",
"function",
"method",
"macro",
"keyword",
"modifier",
"comment",
"string",
"number",
"regexp",
"operator",
"decorator"
],
"enumDescriptions": [
"LSP Semantic Token Type: namespace",
"LSP Semantic Token Type: type",
"LSP Semantic Token Type: class",
"LSP Semantic Token Type: enum",
"LSP Semantic Token Type: interface",
"LSP Semantic Token Type: struct",
"LSP Semantic Token Type: typeParameter",
"LSP Semantic Token Type: parameter",
"LSP Semantic Token Type: variable",
"LSP Semantic Token Type: property",
"LSP Semantic Token Type: enumMember",
"LSP Semantic Token Type: event",
"LSP Semantic Token Type: function",
"LSP Semantic Token Type: method",
"LSP Semantic Token Type: macro",
"LSP Semantic Token Type: keyword",
"LSP Semantic Token Type: modifier",
"LSP Semantic Token Type: comment",
"LSP Semantic Token Type: string",
"LSP Semantic Token Type: number",
"LSP Semantic Token Type: regexp",
"LSP Semantic Token Type: operator",
"LSP Semantic Token Type: decorator"
],
"scope": "resource",
"type": "string"
},
"haskell.plugin.semanticTokens.config.typeVariableToken": {
"default": "typeParameter",
"description": "LSP semantic token type to use for type variables",
"enum": [
"namespace",
"type",
"class",
"enum",
"interface",
"struct",
"typeParameter",
"parameter",
"variable",
"property",
"enumMember",
"event",
"function",
"method",
"macro",
"keyword",
"modifier",
"comment",
"string",
"number",
"regexp",
"operator",
"decorator"
],
"enumDescriptions": [
"LSP Semantic Token Type: namespace",
"LSP Semantic Token Type: type",
"LSP Semantic Token Type: class",
"LSP Semantic Token Type: enum",
"LSP Semantic Token Type: interface",
"LSP Semantic Token Type: struct",
"LSP Semantic Token Type: typeParameter",
"LSP Semantic Token Type: parameter",
"LSP Semantic Token Type: variable",
"LSP Semantic Token Type: property",
"LSP Semantic Token Type: enumMember",
"LSP Semantic Token Type: event",
"LSP Semantic Token Type: function",
"LSP Semantic Token Type: method",
"LSP Semantic Token Type: macro",
"LSP Semantic Token Type: keyword",
"LSP Semantic Token Type: modifier",
"LSP Semantic Token Type: comment",
"LSP Semantic Token Type: string",
"LSP Semantic Token Type: number",
"LSP Semantic Token Type: regexp",
"LSP Semantic Token Type: operator",
"LSP Semantic Token Type: decorator"
],
"scope": "resource",
"type": "string"
},
"haskell.plugin.semanticTokens.config.variableToken": {
"default": "variable",
"description": "LSP semantic token type to use for variables",
"enum": [
"namespace",
"type",
"class",
"enum",
"interface",
"struct",
"typeParameter",
"parameter",
"variable",
"property",
"enumMember",
"event",
"function",
"method",
"macro",
"keyword",
"modifier",
"comment",
"string",
"number",
"regexp",
"operator",
"decorator"
],
"enumDescriptions": [
"LSP Semantic Token Type: namespace",
"LSP Semantic Token Type: type",
"LSP Semantic Token Type: class",
"LSP Semantic Token Type: enum",
"LSP Semantic Token Type: interface",
"LSP Semantic Token Type: struct",
"LSP Semantic Token Type: typeParameter",
"LSP Semantic Token Type: parameter",
"LSP Semantic Token Type: variable",
"LSP Semantic Token Type: property",
"LSP Semantic Token Type: enumMember",
"LSP Semantic Token Type: event",
"LSP Semantic Token Type: function",
"LSP Semantic Token Type: method",
"LSP Semantic Token Type: macro",
"LSP Semantic Token Type: keyword",
"LSP Semantic Token Type: modifier",
"LSP Semantic Token Type: comment",
"LSP Semantic Token Type: string",
"LSP Semantic Token Type: number",
"LSP Semantic Token Type: regexp",
"LSP Semantic Token Type: operator",
"LSP Semantic Token Type: decorator"
],
"scope": "resource",
"type": "string"
},
"haskell.plugin.semanticTokens.globalOn": {
"default": false,
"description": "Enables semanticTokens plugin",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.signatureHelp.globalOn": {
"default": true,
"description": "Enables signatureHelp plugin",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.splice.globalOn": {
"default": true,
"description": "Enables splice plugin",
"scope": "resource",
"type": "boolean"
},
"haskell.plugin.stan.globalOn": {
"default": false,
"description": "Enables stan plugin",
"scope": "resource",
"type": "boolean"
}
}
},
"commands": [
{
"command": "haskell.commands.restartExtension",
"title": "Haskell: Restart vscode-haskell extension",
"description": "Restart the vscode-haskell extension. Reloads configuration."
},
{
"command": "haskell.commands.restartServer",
"title": "Haskell: Restart Haskell LSP server",
"description": "Restart the Haskell LSP server"
},
{
"command": "haskell.commands.startServer",
"title": "Haskell: Start Haskell LSP server",
"description": "Start the Haskell LSP server"
},
{
"command": "haskell.commands.stopServer",
"title": "Haskell: Stop Haskell LSP server",
"description": "Stop the Haskell LSP server"
}
]
},
"scripts": {
"vscode:prepublish": "webpack --mode production",
"package": "npx vsce package --out=dist/",
"build": "npm install",
"webpack": "webpack --mode none",
"watch": "webpack --mode development --watch",
"lint": "eslint -c eslint.config.mjs src",
"lint-fix": "eslint --fix -c eslint.config.mjs src",
"push-tag": "git tag -a $npm_package_version -m \"Version $npm_package_version\" && git push origin $npm_package_version",
"pretest": "tsc --alwaysStrict -p ./",
"format": "prettier . --write",
"test": "vscode-test"
},
"husky": {
"hooks": {
"pre-commit": "pretty-quick --staged"
}
},
"devDependencies": {
"@eslint/js": "^9.37.0",
"@types/mocha": "^10.0.10",
"@types/node": "^22.15.29",
"@types/vscode": "^1.102.0",
"@types/which": "^3.0.4",
"@typescript-eslint/eslint-plugin": "^8.56.0",
"@typescript-eslint/parser": "^8.56.1",
"@vscode/test-cli": "^0.0.12",
"@vscode/test-electron": "^2.5.2",
"corepack": "^0.34.6",
"eslint": "^9.37.0",
"eslint-webpack-plugin": "^5.0.3",
"glob": "^13.0.5",
"globals": "^17.4.0",
"husky": "^9.1.7",
"mocha": "^11.7.5",
"prettier": "^3.8.1",
"ts-loader": "^9.5.2",
"typescript": "^5.9.3",
"typescript-eslint": "^8.57.0",
"webpack": "^5.105.4",
"webpack-cli": "^6.0.1"
},
"extensionDependencies": [
"justusadam.language-haskell"
],
"dependencies": {
"ts-pattern": "^5.9.0",
"vscode-languageclient": "^9.0.1",
"which": "^6.0.1"
},
"packageManager": "npm@11.9.0"
}
================================================
FILE: src/commands/constants.ts
================================================
export const RestartExtensionCommandName = 'haskell.commands.restartExtension';
export const RestartServerCommandName = 'haskell.commands.restartServer';
export const StartServerCommandName = 'haskell.commands.startServer';
export const StopServerCommandName = 'haskell.commands.stopServer';
export const OpenLogsCommandName = 'haskell.commands.openLogs';
export const ShowExtensionVersions = 'haskell.commands.showVersions';
================================================
FILE: src/config.ts
================================================
import { OutputChannel, Uri, window, WorkspaceConfiguration, WorkspaceFolder } from 'vscode';
import { expandHomeDir, IEnvVars } from './utils';
import * as path from 'path';
import { Logger } from 'vscode-languageclient';
import { ExtensionLogger } from './logger';
import { GHCupConfig } from './ghcup';
export type LogLevel = 'off' | 'messages' | 'verbose';
export type ClientLogLevel = 'off' | 'error' | 'info' | 'debug';
export type Config = {
/**
* Unique name per workspace folder (useful for multi-root workspaces).
*/
langName: string;
logLevel: LogLevel;
clientLogLevel: ClientLogLevel;
logFilePath?: string;
workingDir: string;
outputChannel: OutputChannel;
serverArgs: string[];
serverEnvironment: IEnvVars;
ghcupConfig: GHCupConfig;
};
export function initConfig(workspaceConfig: WorkspaceConfiguration, uri: Uri, folder?: WorkspaceFolder): Config {
// Set a unique name per workspace folder (useful for multi-root workspaces).
const langName = 'Haskell' + (folder ? ` (${folder.name})` : '');
const currentWorkingDir = folder ? folder.uri.fsPath : path.dirname(uri.fsPath);
const logLevel = getLogLevel(workspaceConfig);
const clientLogLevel = getClientLogLevel(workspaceConfig);
const logFile = getLogFile(workspaceConfig);
const logFilePath = resolveLogFilePath(logFile, currentWorkingDir);
const outputChannel: OutputChannel = window.createOutputChannel(langName);
const serverArgs = getServerArgs(workspaceConfig, logLevel, logFilePath);
return {
langName: langName,
logLevel: logLevel,
clientLogLevel: clientLogLevel,
logFilePath: logFilePath,
workingDir: currentWorkingDir,
outputChannel: outputChannel,
serverArgs: serverArgs,
serverEnvironment: workspaceConfig.serverEnvironment,
ghcupConfig: {
metadataUrl: workspaceConfig.metadataURL as string,
upgradeGHCup: workspaceConfig.get('upgradeGHCup') as boolean,
executablePath: workspaceConfig.get('ghcupExecutablePath') as string,
},
};
}
export function initLoggerFromConfig(config: Config): ExtensionLogger {
return new ExtensionLogger('client', config.clientLogLevel, config.outputChannel, config.logFilePath);
}
export function logConfig(logger: Logger, config: Config) {
if (config.logFilePath) {
logger.info(`Writing client log to file ${config.logFilePath}`);
}
logger.log('Environment variables:');
Object.entries(process.env).forEach(([key, value]: [string, string | undefined]) => {
// only list environment variables that we actually care about.
// this makes it safe for users to just paste the logs to whoever,
// and avoids leaking secrets.
if (['PATH'].includes(key)) {
logger.log(` ${key}: ${value}`);
}
});
}
function getLogFile(workspaceConfig: WorkspaceConfiguration) {
const logFile_: unknown = workspaceConfig.logFile;
let logFile: string | undefined;
if (typeof logFile_ === 'string') {
logFile = logFile_ !== '' ? logFile_ : undefined;
}
return logFile;
}
function getClientLogLevel(workspaceConfig: WorkspaceConfiguration): ClientLogLevel {
const clientLogLevel_: unknown = workspaceConfig.trace.client;
let clientLogLevel;
if (typeof clientLogLevel_ === 'string') {
switch (clientLogLevel_) {
case 'off':
case 'error':
case 'info':
case 'debug':
clientLogLevel = clientLogLevel_;
break;
default:
throw new Error("Option \"haskell.trace.client\" is expected to be one of 'off', 'error', 'info', 'debug'.");
}
} else {
throw new Error('Option "haskell.trace.client" is expected to be a string');
}
return clientLogLevel;
}
function getLogLevel(workspaceConfig: WorkspaceConfiguration): LogLevel {
const logLevel_: unknown = workspaceConfig.trace.server;
let logLevel;
if (typeof logLevel_ === 'string') {
switch (logLevel_) {
case 'off':
case 'messages':
case 'verbose':
logLevel = logLevel_;
break;
default:
throw new Error("Option \"haskell.trace.server\" is expected to be one of 'off', 'messages', 'verbose'.");
}
} else {
throw new Error('Option "haskell.trace.server" is expected to be a string');
}
return logLevel;
}
function resolveLogFilePath(logFile: string | undefined, currentWorkingDir: string): string | undefined {
return logFile !== undefined ? path.resolve(currentWorkingDir, expandHomeDir(logFile)) : undefined;
}
function getServerArgs(workspaceConfig: WorkspaceConfiguration, logLevel: LogLevel, logFilePath?: string): string[] {
const serverArgs = ['--lsp']
.concat(logLevel === 'messages' ? ['-d'] : [])
.concat(logFilePath !== undefined ? ['-l', logFilePath] : []);
const rawExtraArgs: unknown = workspaceConfig.serverExtraArgs;
if (typeof rawExtraArgs === 'string' && rawExtraArgs !== '') {
const e = rawExtraArgs.split(' ');
serverArgs.push(...e);
}
// We don't want empty strings in our args
return serverArgs.map((x) => x.trim()).filter((x) => x !== '');
}
================================================
FILE: src/docsBrowser.ts
================================================
import { dirname } from 'path';
import {
CancellationToken,
commands,
CompletionContext,
CompletionItem,
CompletionList,
Disposable,
env,
Hover,
MarkdownString,
MarkedString,
Position,
ProviderResult,
TextDocument,
Uri,
ViewColumn,
window,
workspace,
} from 'vscode';
import { ProvideCompletionItemsSignature, ProvideHoverSignature } from 'vscode-languageclient';
async function showDocumentation({
title,
localPath,
hackageUri,
}: {
title: string;
localPath: string;
hackageUri: string;
}) {
const arr = localPath.match(/([^/]+)\.[^.]+$/);
const ttl = arr !== null && arr.length === 2 ? arr[1].replace(/-/gi, '.') : title;
const documentationDirectory = dirname(localPath);
let panel;
try {
const docUri = Uri.parse(documentationDirectory);
// Make sure to use Uri.parse here, as path will already have 'file:///' in it
panel = window.createWebviewPanel('haskell.showDocumentationPanel', ttl, ViewColumn.Beside, {
localResourceRoots: [docUri],
enableFindWidget: true,
enableCommandUris: true,
enableScripts: true,
});
const encoded = encodeURIComponent(JSON.stringify({ hackageUri, inWebView: true }));
const hackageCmd = 'command:haskell.openDocumentationOnHackage?' + encoded;
const bytes = await workspace.fs.readFile(Uri.parse(localPath));
const addBase = `
<base href="${panel.webview.asWebviewUri(Uri.parse(documentationDirectory)).toString()}/">
`;
panel.webview.html = `
<html>
${addBase}
<body>
<div><a href="${hackageCmd}">Open on Hackage</a></div>
${bytes.toString()}
</body>
</html>
`;
} catch (e) {
if (e instanceof Error) {
await window.showErrorMessage(e.message);
}
}
return panel;
}
// registers the browser in VSCode infrastructure
export function registerDocsBrowser(): Disposable {
return commands.registerCommand('haskell.showDocumentation', showDocumentation);
}
async function openDocumentationOnHackage({
hackageUri,
inWebView = false,
}: {
hackageUri: string;
inWebView: boolean;
}) {
try {
// open on Hackage and close the original webview in VS code
await env.openExternal(Uri.parse(hackageUri));
if (inWebView) {
await commands.executeCommand('workbench.action.closeActiveEditor');
}
} catch (e) {
if (e instanceof Error) {
await window.showErrorMessage(e.message);
}
}
}
export function registerDocsOpenOnHackage(): Disposable {
return commands.registerCommand('haskell.openDocumentationOnHackage', openDocumentationOnHackage);
}
export function hoverLinksMiddlewareHook(
document: TextDocument,
position: Position,
token: CancellationToken,
next: ProvideHoverSignature,
): ProviderResult<Hover> {
const res = next(document, position, token);
return Promise.resolve(res).then((r) => {
if (r !== null && r !== undefined) {
r.contents = r.contents.map(processLink);
}
return r;
});
}
export function completionLinksMiddlewareHook(
document: TextDocument,
position: Position,
context: CompletionContext,
token: CancellationToken,
next: ProvideCompletionItemsSignature,
): ProviderResult<CompletionItem[] | CompletionList> {
const res = next(document, position, context, token);
function processCI(ci: CompletionItem): void {
if (ci.documentation) {
ci.documentation = processLink(ci.documentation);
}
}
return Promise.resolve(res).then((r) => {
if (r instanceof Array) {
r.forEach(processCI);
} else if (r) {
r.items.forEach(processCI);
}
return r;
});
}
function processLink(ms: MarkdownString | MarkedString): string | MarkdownString {
const openDocsInHackage = workspace.getConfiguration('haskell').get('openDocumentationInHackage');
const openSourceInHackage = workspace.getConfiguration('haskell').get('openSourceInHackage');
function transform(s: string): string {
return s.replace(
/\[(.+)\]\((file:.+\/doc\/(?:.*html\/libraries\/)?([^/]+)\/(?:.*\/)?(.+\.html#?.*))\)/gi,
(_all, title, localPath, packageName, fileAndAnchor) => {
let hackageUri: string;
if (title === 'Documentation') {
hackageUri = `https://hackage.haskell.org/package/${packageName}/docs/${fileAndAnchor}`;
const encoded = encodeURIComponent(JSON.stringify({ title, localPath, hackageUri }));
let cmd: string;
if (openDocsInHackage) {
cmd = 'command:haskell.openDocumentationOnHackage?' + encoded;
} else {
cmd = 'command:haskell.showDocumentation?' + encoded;
}
return `[${title}](${cmd})`;
} else if (title === 'Source' && typeof fileAndAnchor === 'string') {
const moduleLocation = fileAndAnchor.replace(/-/gi, '.');
hackageUri = `https://hackage.haskell.org/package/${packageName}/docs/src/${moduleLocation}`;
const encoded = encodeURIComponent(JSON.stringify({ title, localPath, hackageUri }));
let cmd: string;
if (openSourceInHackage) {
cmd = 'command:haskell.openDocumentationOnHackage?' + encoded;
} else {
cmd = 'command:haskell.showDocumentation?' + encoded;
}
return `[${title}](${cmd})`;
} else {
return s;
}
},
);
}
if (typeof ms === 'string') {
return transform(ms);
} else if (ms instanceof MarkdownString) {
const mstr = new MarkdownString(transform(ms.value));
mstr.isTrusted = true;
return mstr;
} else {
return ms.value;
}
}
================================================
FILE: src/errors.ts
================================================
import { Uri } from 'vscode';
export class HlsError extends Error {}
export class MissingToolError extends HlsError {
public readonly tool: string;
constructor(tool: string) {
let prettyTool: string;
switch (tool.toLowerCase()) {
case 'stack':
prettyTool = 'Stack';
break;
case 'cabal':
prettyTool = 'Cabal';
break;
case 'ghc':
prettyTool = 'GHC';
break;
case 'ghcup':
prettyTool = 'GHCup';
break;
case 'haskell-language-server':
case 'hls':
prettyTool = 'HLS';
break;
default:
prettyTool = tool;
break;
}
super(`Project requires ${prettyTool} but it isn't installed`);
this.tool = prettyTool;
}
public installLink(): Uri | null {
switch (this.tool) {
case 'Stack':
return Uri.parse('https://docs.haskellstack.org/en/stable/install_and_upgrade/');
case 'GHCup':
case 'Cabal':
case 'HLS':
case 'GHC':
return Uri.parse('https://www.haskell.org/ghcup/');
default:
return null;
}
}
}
export class NoMatchingHls extends Error {
constructor(readonly ghcProjVersion: string) {
super(`HLS does not support GHC ${ghcProjVersion} yet.`);
}
public docLink(): Uri {
return Uri.parse('https://haskell-language-server.readthedocs.io/en/latest/support/ghc-version-support.html');
}
}
================================================
FILE: src/extension.ts
================================================
import { commands, env, ExtensionContext, TextDocument, Uri, window, workspace, WorkspaceFolder } from 'vscode';
import {
ExecutableOptions,
LanguageClient,
LanguageClientOptions,
Logger,
RevealOutputChannelOn,
ServerOptions,
} from 'vscode-languageclient/node';
import * as constants from './commands/constants';
import * as DocsBrowser from './docsBrowser';
import { HlsError, MissingToolError, NoMatchingHls } from './errors';
import { findHaskellLanguageServer, HlsExecutable, IEnvVars, fetchConfig } from './hlsBinaries';
import { addPathToProcessPath, comparePVP, callAsync } from './utils';
import { Config, initConfig, initLoggerFromConfig, logConfig } from './config';
import { HaskellStatusBar } from './statusBar';
/**
* Global information about the running clients.
*/
type Client = {
client: LanguageClient;
config: Config;
};
// The current map of documents & folders to language servers.
// It may be null to indicate that we are in the process of launching a server,
// in which case don't try to launch another one for that uri
const clients: Map<string, Client | null> = new Map();
// This is the entrypoint to our extension
export async function activate(context: ExtensionContext) {
const statusBar = new HaskellStatusBar(context.extension.packageJSON.version as string | undefined);
context.subscriptions.push(statusBar);
// (Possibly) launch the language server every time a document is opened, so
// it works across multiple workspace folders. Eventually, haskell-lsp should
// just support
// https://microsoft.github.io/language-server-protocol/specifications/specification-3-15/#workspace_workspaceFolders
// and then we can just launch one server
workspace.onDidOpenTextDocument(async (document: TextDocument) => await activateServer(context, document));
for (const document of workspace.textDocuments) {
await activateServer(context, document);
}
// Stop the server from any workspace folders that are removed.
workspace.onDidChangeWorkspaceFolders(async (event) => {
for (const folder of event.removed) {
const client = clients.get(folder.uri.toString());
if (client) {
const uri = folder.uri.toString();
client.client.info(`Deleting folder for clients: ${uri}`);
clients.delete(uri);
client.client.info('Stopping the server');
await client.client.stop();
}
}
});
// Register editor commands for HIE, but only register the commands once at activation.
const restartCmd = commands.registerCommand(constants.RestartServerCommandName, async () => {
for (const langClient of clients.values()) {
langClient?.client.info('Stopping the server');
await langClient?.client.stop();
langClient?.client.info('Starting the server');
await langClient?.client.start();
}
});
context.subscriptions.push(restartCmd);
const openLogsCmd = commands.registerCommand(constants.OpenLogsCommandName, () => {
for (const langClient of clients.values()) {
langClient?.config.outputChannel.show();
}
});
context.subscriptions.push(openLogsCmd);
const restartExtensionCmd = commands.registerCommand(constants.RestartExtensionCommandName, async () => {
for (const langClient of clients.values()) {
langClient?.client.info('Stopping the server');
await langClient?.client.stop();
}
clients.clear();
fetchConfig();
for (const document of workspace.textDocuments) {
await activateServer(context, document);
}
});
context.subscriptions.push(restartExtensionCmd);
const showVersionsCmd = commands.registerCommand(constants.ShowExtensionVersions, () => {
void window.showInformationMessage(`Extension Version: ${context.extension.packageJSON.version ?? '<unknown>'}`);
});
context.subscriptions.push(showVersionsCmd);
const stopCmd = commands.registerCommand(constants.StopServerCommandName, async () => {
for (const langClient of clients.values()) {
langClient?.client.info('Stopping the server');
await langClient?.client.stop();
langClient?.client.info('Server stopped');
}
});
context.subscriptions.push(stopCmd);
const startCmd = commands.registerCommand(constants.StartServerCommandName, async () => {
for (const langClient of clients.values()) {
langClient?.client.info('Starting the server');
await langClient?.client.start();
langClient?.client.info('Server started');
}
});
context.subscriptions.push(startCmd);
// Set up the documentation browser.
const docsDisposable = DocsBrowser.registerDocsBrowser();
context.subscriptions.push(docsDisposable);
const openOnHackageDisposable = DocsBrowser.registerDocsOpenOnHackage();
context.subscriptions.push(openOnHackageDisposable);
statusBar.refresh();
statusBar.show();
}
async function activateServer(context: ExtensionContext, document: TextDocument) {
// We are only interested in Haskell files.
if (
(document.languageId !== 'haskell' &&
document.languageId !== 'cabal' &&
document.languageId !== 'literate haskell') ||
(document.uri.scheme !== 'file' && document.uri.scheme !== 'untitled')
) {
return;
}
const uri = document.uri;
const folder = workspace.getWorkspaceFolder(uri);
await activateServerForFolder(context, uri, folder);
}
async function activateServerForFolder(context: ExtensionContext, uri: Uri, folder?: WorkspaceFolder) {
const clientsKey = folder ? folder.uri.toString() : uri.toString();
// If the client already has an LSP server for this uri/folder, then don't start a new one.
if (clients.has(clientsKey)) {
return;
}
// Set the key to null to prevent multiple servers being launched at once
clients.set(clientsKey, null);
const config = initConfig(workspace.getConfiguration('haskell', uri), uri, folder);
const logger: Logger = initLoggerFromConfig(config);
logConfig(logger, config);
let hlsExecutable: HlsExecutable;
try {
hlsExecutable = await findHaskellLanguageServer(context, logger, config.ghcupConfig, config.workingDir, folder);
} catch (e) {
await handleInitializationError(e, logger);
// Make sure to release the key again.
clients.delete(clientsKey);
return;
}
const serverEnvironment: IEnvVars = initServerEnvironment(config, hlsExecutable);
const exeOptions: ExecutableOptions = {
cwd: config.workingDir,
env: { ...process.env, ...serverEnvironment },
};
// For our intents and purposes, the server should be launched the same way in
// both debug and run mode.
const serverOptions: ServerOptions = {
run: { command: hlsExecutable.location, args: config.serverArgs, options: exeOptions },
debug: { command: hlsExecutable.location, args: config.serverArgs, options: exeOptions },
};
// If we're operating on a standalone file (i.e. not in a folder) then we need
// to launch the server in a reasonable current directory. Otherwise the cradle
// guessing logic in hie-bios will be wrong!
let cwdMsg = `Activating the language server in working dir: ${config.workingDir}`;
if (folder) {
cwdMsg += ' (the workspace folder)';
} else {
cwdMsg += ` (parent dir of loaded file ${uri.fsPath})`;
}
logger.info(cwdMsg);
logger.info(`run command: ${hlsExecutable.location} ${config.serverArgs.join(' ')}`);
logger.info(`debug command: ${hlsExecutable.location} ${config.serverArgs.join(' ')}`);
if (exeOptions.cwd) {
logger.info(`server cwd: ${exeOptions.cwd}`);
}
if (serverEnvironment) {
logger.info('server environment variables:');
Object.entries(serverEnvironment).forEach(([key, val]: [string, string | undefined]) => {
logger.info(` ${key}=${val}`);
});
}
const pat = folder ? `${folder.uri.fsPath}/**/*` : '**/*';
logger.log(`document selector patten: ${pat}`);
const cabalDocumentSelector = { scheme: 'file', language: 'cabal', pattern: pat };
const haskellDocumentSelector = [
{ scheme: 'file', language: 'haskell', pattern: pat },
{ scheme: 'file', language: 'literate haskell', pattern: pat },
];
const documentSelector = [...haskellDocumentSelector];
const cabalFileSupport: 'automatic' | 'enable' | 'disable' = workspace.getConfiguration(
'haskell',
uri,
).supportCabalFiles;
logger.info(`Support for '.cabal' files: ${cabalFileSupport}`);
switch (cabalFileSupport) {
case 'automatic': {
const hlsVersion = await callAsync(
hlsExecutable.location,
['--numeric-version'],
logger,
config.workingDir,
undefined /* this command is very fast, don't show anything */,
false,
serverEnvironment,
);
if (comparePVP(hlsVersion, '1.9.0.0') >= 0) {
// If hlsVersion is >= '1.9.0.0'
documentSelector.push(cabalDocumentSelector);
}
break;
}
case 'enable':
documentSelector.push(cabalDocumentSelector);
break;
case 'disable':
break;
default:
break;
}
const clientOptions: LanguageClientOptions = {
// Use the document selector to only notify the LSP on files inside the folder
// path for the specific workspace.
documentSelector: [...documentSelector],
synchronize: {
// Synchronize the setting section 'haskell' to the server.
configurationSection: 'haskell',
},
diagnosticCollectionName: config.langName,
revealOutputChannelOn: RevealOutputChannelOn.Never,
outputChannel: config.outputChannel,
outputChannelName: config.langName,
middleware: {
provideHover: DocsBrowser.hoverLinksMiddlewareHook,
provideCompletionItem: DocsBrowser.completionLinksMiddlewareHook,
},
// Launch the server in the directory of the workspace folder.
workspaceFolder: folder,
};
// Create the LSP client.
const langClient = new LanguageClient('haskell', config.langName, serverOptions, clientOptions);
// Register ClientCapabilities for stuff like window/progress
langClient.registerProposedFeatures();
// Finally start the client and add it to the list of clients.
logger.info('Starting language server');
clients.set(clientsKey, {
client: langClient,
config,
});
await langClient.start();
}
/**
* Handle errors the extension may throw. Errors are expected to be fatal.
*
* @param e Error thrown during the extension initialization.
* @param logger
*/
async function handleInitializationError(e: unknown, logger: Logger) {
if (e instanceof MissingToolError) {
const link = e.installLink();
if (link) {
if (await window.showErrorMessage(e.message, `Install ${e.tool}`)) {
env.openExternal(link);
}
} else {
await window.showErrorMessage(e.message);
}
} else if (e instanceof HlsError) {
logger.error(`General HlsError: ${e.message}`);
window.showErrorMessage(e.message);
} else if (e instanceof NoMatchingHls) {
const link = e.docLink();
logger.error(`${e.message}`);
if (await window.showErrorMessage(e.message, 'Open documentation')) {
env.openExternal(link);
}
} else if (e instanceof Error) {
logger.error(`Internal Error: ${e.message}`);
window.showErrorMessage(e.message);
}
if (e instanceof Error) {
// general stack trace printing
if (e.stack) {
logger.error(`${e.stack}`);
}
}
}
function initServerEnvironment(config: Config, hlsExecutable: HlsExecutable) {
let serverEnvironment: IEnvVars = config.serverEnvironment;
if (hlsExecutable.tag === 'ghcup') {
const newPath = addPathToProcessPath(hlsExecutable.binaryDirectory);
serverEnvironment = {
...serverEnvironment,
...{ PATH: newPath },
};
}
return serverEnvironment;
}
/*
* Deactivate each of the LSP servers.
*/
export async function deactivate() {
const promises: Thenable<void>[] = [];
for (const client of clients.values()) {
if (client) {
promises.push(client.client.stop());
}
}
await Promise.all(promises);
}
================================================
FILE: src/ghcup.ts
================================================
import * as path from 'path';
import * as os from 'os';
import * as process from 'process';
import { WorkspaceFolder } from 'vscode';
import { Logger } from 'vscode-languageclient';
import { MissingToolError } from './errors';
import { resolvePathPlaceHolders, executableExists, callAsync, ProcessCallback, IEnvVars } from './utils';
import { match } from 'ts-pattern';
export type Tool = 'hls' | 'ghc' | 'cabal' | 'stack';
export type ToolConfig = Map<Tool, string | null>;
export function initDefaultGHCup(config: GHCupConfig, logger: Logger, folder?: WorkspaceFolder): GHCup {
const ghcupLoc = findGHCup(logger, config.executablePath, folder);
return new GHCup(logger, ghcupLoc, config, {
// omit colourful output because the logs are uglier
NO_COLOR: '1',
});
}
export type GHCupConfig = {
metadataUrl?: string;
upgradeGHCup: boolean;
executablePath?: string;
};
export type ToolInfo = {
tool: Tool;
version: string;
tags: string[];
};
export class GHCup {
constructor(
readonly logger: Logger,
readonly location: string,
readonly config: GHCupConfig,
readonly environment: IEnvVars,
) {}
/**
* Most generic way to run the `ghcup` binary.
* @param args Arguments to run the `ghcup` binary with.
* @param title Displayed to the user for long-running tasks.
* @param cancellable Whether this invocation can be cancelled by the user.
* @param callback Handle success or failures.
* @returns The output of the `ghcup` invocation. If no {@link callback} is given, this is the stdout. Otherwise, whatever {@link callback} produces.
*/
public async call(
args: string[],
title?: string,
cancellable?: boolean,
callback?: ProcessCallback,
): Promise<string> {
const metadataUrl = this.config.metadataUrl; // ;
return await callAsync(
this.location,
['--no-verbose'].concat(metadataUrl ? ['-s', metadataUrl] : []).concat(args),
this.logger,
undefined,
title,
cancellable,
this.environment,
callback,
);
}
/**
* Upgrade the `ghcup` binary unless this option was disabled by the user.
*/
public async upgrade(): Promise<void> {
const upgrade = this.config.upgradeGHCup;
if (upgrade) {
await this.call(['upgrade'], 'Upgrading ghcup', true);
}
}
/**
* Find the `set` version of a {@link Tool} in GHCup.
* If no version is set, return null.
* @param tool Tool you want to know the latest version of.
* @returns The latest installed or generally available version of the {@link tool}
*/
public async getSetVersion(tool: Tool): Promise<ToolInfo | null> {
// these might be custom/stray/compiled, so we try first
const installedVersions = await this.listTool(tool, 'set');
const latestInstalled = installedVersions.pop();
if (latestInstalled) {
return latestInstalled;
} else {
return null;
}
}
/**
* Find the latest version of a {@link Tool} that we can find in GHCup.
* Prefer already installed versions, but fall back to all available versions, if there aren't any.
* @param tool Tool you want to know the latest version of.
* @returns The latest installed or generally available version of the {@link tool}
*/
public async getAnyLatestVersion(tool: Tool): Promise<ToolInfo | null> {
// these might be custom/stray/compiled, so we try first
const installedVersions = await this.listTool(tool, 'installed');
const latestInstalled = installedVersions.pop();
if (latestInstalled) {
return latestInstalled;
} else {
return this.getLatestAvailableVersion(tool);
}
}
/**
* Find the latest available version that we can find in GHCup with a certain {@link tag}.
* Corresponds to the `ghcup list -t <tool> -c available -r` command.
* The tag can be used to further filter the list of versions, for example you can provide
* @param tool Tool you want to know the latest version of.
* @param tag The tag to filter the available versions with. By default `"latest"`.
* @returns The latest available version filtered by {@link tag}.
*/
public async getLatestAvailableVersion(tool: Tool, tag: string = 'latest'): Promise<ToolInfo> {
// fall back to installable versions
const availableVersions = await this.listTool(tool, 'available');
let latestAvailable: ToolInfo | null = null;
availableVersions.forEach((toolInfo) => {
if (toolInfo.tags.includes(tag)) {
latestAvailable = toolInfo;
}
});
if (!latestAvailable) {
throw new Error(`Unable to find ${tag} tool ${tool}`);
} else {
return latestAvailable;
}
}
private async listTool(tool: Tool, category: string): Promise<ToolInfo[]> {
// fall back to installable versions
const availableVersions = await this.call(['list', '-t', tool, '-c', category, '-r'], undefined, false).then((s) =>
s.split(/\r?\n/),
);
return availableVersions.map((toolString) => {
const toolParts = toolString.split(/\s+/);
return {
tool: tool,
version: toolParts[1],
tags: toolParts[2]?.split(',') ?? [],
};
});
}
public async findLatestUserInstalledTool(tool: Tool): Promise<ToolInfo> {
let toolInfo = null;
toolInfo = await this.getSetVersion(tool);
if (toolInfo) return toolInfo;
toolInfo = await this.getAnyLatestVersion(tool);
if (toolInfo) return toolInfo;
throw new Error(`Unable to find a version for tool ${tool}`);
}
}
function findGHCup(logger: Logger, exePath?: string, folder?: WorkspaceFolder): string {
logger.info('Checking for ghcup installation');
if (exePath) {
logger.info(`Trying to find the ghcup executable in: ${exePath}`);
exePath = resolvePathPlaceHolders(exePath, folder);
logger.log(`Location after path variables substitution: ${exePath}`);
if (executableExists(exePath)) {
return exePath;
} else {
throw new Error(`Could not find a ghcup binary at ${exePath}!`);
}
} else {
const localGHCup = ['ghcup'].find(executableExists);
if (!localGHCup) {
logger.info(`probing for GHCup binary`);
const ghcupExe: string | null = match(process.platform)
.with('win32', () => {
const ghcupPrefix = process.env.GHCUP_INSTALL_BASE_PREFIX;
if (ghcupPrefix) {
return path.join(ghcupPrefix, 'ghcup', 'bin', 'ghcup.exe');
} else {
return path.join('C:\\', 'ghcup', 'bin', 'ghcup.exe');
}
})
.otherwise(() => {
const useXDG = process.env.GHCUP_USE_XDG_DIRS;
if (useXDG) {
const xdgBin = process.env.XDG_BIN_HOME;
if (xdgBin) {
return path.join(xdgBin, 'ghcup');
} else {
return path.join(os.homedir(), '.local', 'bin', 'ghcup');
}
} else {
const ghcupPrefix = process.env.GHCUP_INSTALL_BASE_PREFIX;
if (ghcupPrefix) {
return path.join(ghcupPrefix, '.ghcup', 'bin', 'ghcup');
} else {
return path.join(os.homedir(), '.ghcup', 'bin', 'ghcup');
}
}
});
if (ghcupExe !== null && executableExists(ghcupExe)) {
return ghcupExe;
} else {
logger.warn(`ghcup at ${ghcupExe} does not exist`);
throw new MissingToolError('ghcup');
}
} else {
logger.info(`found ghcup at ${localGHCup}`);
return localGHCup;
}
}
}
================================================
FILE: src/hlsBinaries.ts
================================================
import * as fs from 'fs';
import * as path from 'path';
import { ConfigurationTarget, ExtensionContext, window, workspace, WorkspaceFolder } from 'vscode';
import { Logger } from 'vscode-languageclient';
import { HlsError, MissingToolError, NoMatchingHls } from './errors';
import {
addPathToProcessPath,
callAsync,
comparePVP,
executableExists,
IEnvVars,
resolvePathPlaceHolders,
} from './utils';
import { ToolConfig, Tool, initDefaultGHCup, GHCup, GHCupConfig } from './ghcup';
import { getHlsMetadata } from './metadata';
export { IEnvVars, fetchConfig };
export type Context = {
manageHls: ManageHLS;
storagePath: string;
serverExecutable?: HlsExecutable;
logger: Logger;
};
/**
* Global configuration for this extension.
*/
let haskellConfig = workspace.getConfiguration('haskell');
/**
* On Windows the executable needs to be stored somewhere with an .exe extension
*/
const exeExt = process.platform === 'win32' ? '.exe' : '';
type ManageHLS = 'GHCup' | 'PATH';
let manageHLS = haskellConfig.get('manageHLS') as ManageHLS;
function fetchConfig() {
haskellConfig = workspace.getConfiguration('haskell');
manageHLS = haskellConfig.get('manageHLS') as ManageHLS;
}
/**
* Gets serverExecutablePath and fails if it's not set.
* @param logger Log progress.
* @param folder Workspace folder. Used for resolving variables in the `serverExecutablePath`.
* @returns Path to an HLS executable binary.
*/
function findServerExecutable(logger: Logger, folder?: WorkspaceFolder): string {
const rawExePath = haskellConfig.get('serverExecutablePath') as string;
logger.info(`Trying to find the server executable in: ${rawExePath}`);
const resolvedExePath = resolvePathPlaceHolders(rawExePath, folder);
logger.log(`Location after path variables substitution: ${resolvedExePath}`);
if (executableExists(resolvedExePath)) {
return resolvedExePath;
} else {
const msg = `Could not find a HLS binary at ${resolvedExePath}! Consider installing HLS via ghcup or change "haskell.manageHLS" in your settings.`;
throw new HlsError(msg);
}
}
/**
* Searches the `PATH` for `haskell-language-server` or `haskell-language-server-wrapper` binary.
* Fails if nothing is found.
* @param logger Log all the stuff!
* @returns Location of the `haskell-language-server` or `haskell-language-server-wrapper` binary if found.
*/
function findHlsInPath(logger: Logger): string {
// try PATH
const exes: string[] = ['haskell-language-server-wrapper', 'haskell-language-server'];
logger.info(`Searching for server executables ${exes.join(',')} in $PATH`);
logger.info(`$PATH environment variable: ${process.env.PATH}`);
for (const exe of exes) {
if (executableExists(exe)) {
logger.info(`Found server executable in $PATH: ${exe}`);
return exe;
}
}
throw new MissingToolError('hls');
}
export type HlsExecutable = HlsOnPath | HlsViaVSCodeConfig | HlsViaGhcup;
export type HlsOnPath = {
location: string;
tag: 'path';
};
export type HlsViaVSCodeConfig = {
location: string;
tag: 'config';
};
export type HlsViaGhcup = {
location: string;
/**
* if we download HLS, add that bin dir to PATH
*/
binaryDirectory: string;
tag: 'ghcup';
};
/**
* Find and setup the Haskell Language Server.
*
* We support three ways of finding the HLS binary:
*
* 1. Let the user provide a location via `haskell.serverExecutablePath` option.
* 2. Find a `haskell-language-server` binary on the `$PATH` if the user wants to do that.
* 3. Use GHCup to install and locate HLS and other required tools, such as cabal, stack and ghc.
*
* @param context Context of the extension, required for metadata.
* @param logger Logger for progress updates.
* @param workingDir Working directory in VSCode.
* @param folder Optional workspace folder. If given, will be preferred over {@link workingDir} for finding configuration entries.
* @returns Path to haskell-language-server, paired with additional data required for setting up.
*/
export async function findHaskellLanguageServer(
context: ExtensionContext,
logger: Logger,
ghcupConfig: GHCupConfig,
workingDir: string,
folder?: WorkspaceFolder,
): Promise<HlsExecutable> {
logger.info('Finding haskell-language-server');
const hasConfigForExecutable = haskellConfig.get('serverExecutablePath') as string;
if (hasConfigForExecutable) {
const exe = findServerExecutable(logger, folder);
return {
location: exe,
tag: 'config',
};
}
const storagePath: string = getStoragePath(context);
if (!fs.existsSync(storagePath)) {
fs.mkdirSync(storagePath);
}
// first extension initialization
manageHLS = await promptUserForManagingHls(context, manageHLS);
// based on the user-decision
if (manageHLS === 'PATH') {
const exe = findHlsInPath(logger);
return {
location: exe,
tag: 'path',
};
} else {
// we manage HLS, make sure ghcup is installed/available
const ghcup = initDefaultGHCup(ghcupConfig, logger, folder);
await ghcup.upgrade();
// boring init
let latestHLS: string | undefined | null;
let latestCabal: string | undefined | null;
let latestStack: string | undefined | null;
let recGHC: string | undefined | null = 'recommended';
let projectHls: string | undefined | null;
let projectGhc: string | undefined | null;
// support explicit toolchain config
const toolchainConfig = new Map(Object.entries(haskellConfig.get('toolchain') as ToolConfig)) as ToolConfig;
if (toolchainConfig) {
latestHLS = toolchainConfig.get('hls');
latestCabal = toolchainConfig.get('cabal');
latestStack = toolchainConfig.get('stack');
recGHC = toolchainConfig.get('ghc');
projectHls = latestHLS;
projectGhc = recGHC;
}
// get a preliminary toolchain for finding the correct project GHC version
// (we need HLS and cabal/stack and ghc as fallback),
// later we may install a different toolchain that's more project-specific
if (latestHLS === undefined) {
latestHLS = await ghcup.getAnyLatestVersion('hls').then((tool) => tool?.version);
}
if (latestCabal === undefined) {
latestCabal = (await ghcup.findLatestUserInstalledTool('cabal')).version;
}
if (latestStack === undefined) {
latestStack = (await ghcup.findLatestUserInstalledTool('stack')).version;
}
if (recGHC === undefined) {
recGHC = !executableExists('ghc') ? (await ghcup.getLatestAvailableVersion('ghc', 'recommended')).version : null;
}
// download popups
const promptBeforeDownloads = haskellConfig.get('promptBeforeDownloads') as boolean;
if (promptBeforeDownloads) {
const hlsInstalled = latestHLS ? await installationStatusOfGhcupTool(ghcup, 'hls', latestHLS) : undefined;
const cabalInstalled = latestCabal ? await installationStatusOfGhcupTool(ghcup, 'cabal', latestCabal) : undefined;
const stackInstalled = latestStack ? await installationStatusOfGhcupTool(ghcup, 'stack', latestStack) : undefined;
const ghcInstalled = executableExists('ghc')
? new ToolStatus(
'ghc',
await callAsync(`ghc${exeExt}`, ['--numeric-version'], logger, undefined, undefined, false),
)
: // if recGHC is null, that means user disabled automatic handling,
recGHC !== null
? await installationStatusOfGhcupTool(ghcup, 'ghc', recGHC)
: undefined;
const toInstall: ToolStatus[] = [hlsInstalled, cabalInstalled, stackInstalled, ghcInstalled].filter(
(tool) => tool && !tool.installed,
) as ToolStatus[];
if (toInstall.length > 0) {
const decision = await window.showInformationMessage(
`Need to download ${toInstall.map((t) => t.nameWithVersion).join(', ')}, continue?`,
'Yes',
'No',
"Yes, don't ask again",
);
if (decision === 'Yes') {
logger.info(`User accepted download for ${toInstall.map((t) => t.nameWithVersion).join(', ')}.`);
} else if (decision === "Yes, don't ask again") {
logger.info(
`User accepted download for ${toInstall.map((t) => t.nameWithVersion).join(', ')} and won't be asked again.`,
);
haskellConfig.update('promptBeforeDownloads', false);
} else {
toInstall.forEach((tool) => {
if (tool !== undefined && !tool.installed) {
if (tool.name === 'hls') {
throw new MissingToolError('hls');
} else if (tool.name === 'cabal') {
latestCabal = null;
} else if (tool.name === 'stack') {
latestStack = null;
} else if (tool.name === 'ghc') {
recGHC = null;
}
}
});
}
}
}
// our preliminary toolchain
const latestToolchainBindir = await ghcup.call(
[
'run',
...(latestHLS ? ['--hls', latestHLS] : []),
...(latestCabal ? ['--cabal', latestCabal] : []),
...(latestStack ? ['--stack', latestStack] : []),
...(recGHC ? ['--ghc', recGHC] : []),
'--install',
],
'Installing latest toolchain for bootstrap',
true,
(err, stdout, _stderr, resolve, reject) => {
if (err) {
reject("Couldn't install latest toolchain");
} else {
resolve(stdout?.trim());
}
},
);
// now figure out the actual project GHC version and the latest supported HLS version
// we need for it (e.g. this might in fact be a downgrade for old GHCs)
if (projectHls === undefined || projectGhc === undefined) {
const res = await getLatestProjectHls(ghcup, logger, storagePath, workingDir, latestToolchainBindir);
if (projectHls === undefined) {
projectHls = res[0];
}
if (projectGhc === undefined) {
projectGhc = res[1];
}
}
// more download popups
if (promptBeforeDownloads) {
const hlsInstalled = projectHls ? await installationStatusOfGhcupTool(ghcup, 'hls', projectHls) : undefined;
const ghcInstalled = projectGhc ? await installationStatusOfGhcupTool(ghcup, 'ghc', projectGhc) : undefined;
const toInstall: ToolStatus[] = [hlsInstalled, ghcInstalled].filter(
(tool) => tool && !tool.installed,
) as ToolStatus[];
if (toInstall.length > 0) {
const decision = await window.showInformationMessage(
`Need to download ${toInstall.map((t) => t.nameWithVersion).join(', ')}, continue?`,
{ modal: true },
'Yes',
'No',
"Yes, don't ask again",
);
if (decision === 'Yes') {
logger.info(`User accepted download for ${toInstall.map((t) => t.nameWithVersion).join(', ')}.`);
} else if (decision === "Yes, don't ask again") {
logger.info(
`User accepted download for ${toInstall.map((t) => t.nameWithVersion).join(', ')} and won't be asked again.`,
);
haskellConfig.update('promptBeforeDownloads', false);
} else {
toInstall.forEach((tool) => {
if (!tool.installed) {
if (tool.name === 'hls') {
throw new MissingToolError('hls');
} else if (tool.name === 'ghc') {
projectGhc = null;
}
}
});
}
}
}
// now install the proper versions
const hlsBinDir = await ghcup.call(
[
'run',
...(projectHls ? ['--hls', projectHls] : []),
...(latestCabal ? ['--cabal', latestCabal] : []),
...(latestStack ? ['--stack', latestStack] : []),
...(projectGhc ? ['--ghc', projectGhc] : []),
'--install',
],
`Installing project specific toolchain: ${[
['hls', projectHls],
['GHC', projectGhc],
['cabal', latestCabal],
['stack', latestStack],
]
.filter((t) => t[1])
.map((t) => `${t[0]}-${t[1]}`)
.join(', ')}`,
true,
);
if (projectHls) {
return {
binaryDirectory: hlsBinDir,
location: path.join(hlsBinDir, `haskell-language-server-wrapper${exeExt}`),
tag: 'ghcup',
};
} else {
return {
binaryDirectory: hlsBinDir,
location: findHlsInPath(logger),
tag: 'ghcup',
};
}
}
}
async function promptUserForManagingHls(context: ExtensionContext, manageHlsSetting: ManageHLS): Promise<ManageHLS> {
if (manageHlsSetting !== 'GHCup' && (!context.globalState.get('pluginInitialized') as boolean | null)) {
const promptMessage = `How do you want the extension to manage/discover HLS and the relevant toolchain?
Choose "Automatically" if you're in doubt.
`;
const popup = window.showInformationMessage(
promptMessage,
{ modal: true },
'Automatically via GHCup',
'Manually via PATH',
);
const decision = (await popup) || null;
let howToManage: ManageHLS;
if (decision === 'Automatically via GHCup') {
howToManage = 'GHCup';
} else if (decision === 'Manually via PATH') {
howToManage = 'PATH';
} else {
window.showWarningMessage(
"Choosing default PATH method for HLS discovery. You can change this via 'haskell.manageHLS' in the settings.",
);
howToManage = 'PATH';
}
haskellConfig.update('manageHLS', howToManage, ConfigurationTarget.Global);
context.globalState.update('pluginInitialized', true);
return howToManage;
} else {
return manageHlsSetting;
}
}
async function getLatestProjectHls(
ghcup: GHCup,
logger: Logger,
storagePath: string,
workingDir: string,
toolchainBindir: string,
): Promise<[string, string]> {
// get project GHC version, but fallback to system ghc if necessary.
const projectGhc = toolchainBindir
? await getProjectGhcVersion(toolchainBindir, workingDir, logger).catch(async (e) => {
logger.error(`${e}`);
window.showWarningMessage(
`I had trouble figuring out the exact GHC version for the project. Falling back to using 'ghc${exeExt}'.`,
);
return await callAsync(`ghc${exeExt}`, ['--numeric-version'], logger, undefined, undefined, false);
})
: await callAsync(`ghc${exeExt}`, ['--numeric-version'], logger, undefined, undefined, false);
// first we get supported GHC versions from available HLS bindists (whether installed or not)
const metadataMap = (await getHlsMetadata(storagePath, logger)) || new Map<string, string[]>();
// then we get supported GHC versions from currently installed HLS versions
const ghcupMap = (await findAvailableHlsBinariesFromGHCup(ghcup)) || new Map<string, string[]>();
// since installed HLS versions may support a different set of GHC versions than the bindists
// (e.g. because the user ran 'ghcup compile hls'), we need to merge both maps, preferring
// values from already installed HLSes
const merged = new Map<string, string[]>([...metadataMap, ...ghcupMap]); // right-biased
// now sort and get the latest suitable version
const latest = [...merged]
.filter(([_k, v]) => v.some((x) => x === projectGhc))
.sort(([k1, _v1], [k2, _v2]) => comparePVP(k1, k2))
.pop();
if (!latest) {
throw new NoMatchingHls(projectGhc);
} else {
return [latest[0], projectGhc];
}
}
/**
* Obtain the project ghc version from the HLS - Wrapper (which must be in PATH now).
* Also, serves as a sanity check.
* @param toolchainBindir Path to the toolchain bin directory (added to PATH)
* @param workingDir Directory to run the process, usually the root of the workspace.
* @param logger Logger for feedback.
* @returns The GHC version, or fail with an `Error`.
*/
export async function getProjectGhcVersion(
toolchainBindir: string,
workingDir: string,
logger: Logger,
): Promise<string> {
const title = 'Working out the project GHC version. This might take a while...';
logger.info(title);
const args = ['--project-ghc-version'];
const newPath = addPathToProcessPath(toolchainBindir);
const environmentNew: IEnvVars = {
PATH: newPath,
};
return callAsync(
'haskell-language-server-wrapper',
args,
logger,
workingDir,
title,
false,
environmentNew,
(err, stdout, stderr, resolve, reject) => {
if (err) {
// Error message emitted by HLS-wrapper
const regex =
/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/;
const res = regex.exec(stderr);
if (res) {
for (let i = 1; i < res.length; i++) {
if (res[i]) {
reject(new MissingToolError(res[i]));
}
}
reject(new MissingToolError('unknown'));
}
reject(
Error(
`haskell-language-server --project-ghc-version exited with exit code ${err.code}:\n${stdout}\n${stderr}`,
),
);
} else {
logger.info(`The GHC version for the project or file: ${stdout?.trim()}`);
resolve(stdout?.trim());
}
},
);
}
/**
* Find the storage path for the extension.
* If no custom location was given
*
* @param context Extension context for the 'Storage Path'.
* @returns
*/
export function getStoragePath(context: ExtensionContext): string {
let storagePath: string | undefined = haskellConfig.get('releasesDownloadStoragePath');
if (!storagePath) {
storagePath = context.globalStorageUri.fsPath;
} else {
storagePath = resolvePathPlaceHolders(storagePath);
}
return storagePath;
}
/**
*
* Complements {@link getReleaseMetadata}, by checking possibly locally compiled
* HLS in ghcup
* If 'targetGhc' is omitted, picks the latest 'haskell-language-server-wrapper',
* otherwise ensures the specified GHC is supported.
*
* @param ghcup GHCup wrapper.
* @returns A Map of the locally installed HLS versions and with which `GHC` versions they are compatible.
*/
async function findAvailableHlsBinariesFromGHCup(ghcup: GHCup): Promise<Map<string, string[]> | null> {
const hlsVersions = await ghcup.call(['list', '-t', 'hls', '-c', 'installed', '-r'], undefined, false);
const bindir = await ghcup.call(['whereis', 'bindir'], undefined, false);
const files = fs.readdirSync(bindir).filter((e) => {
const stat = fs.statSync(path.join(bindir, e));
return stat.isFile();
});
const installed = hlsVersions.split(/\r?\n/).map((e) => e.split(/\s+/)[1]);
if (installed?.length) {
const myMap = new Map<string, string[]>();
installed.forEach((hls) => {
const ghcs = files
.filter((f) => f.endsWith(`~${hls}${exeExt}`) && f.startsWith('haskell-language-server-'))
.map((f) => {
const rmPrefix = f.substring('haskell-language-server-'.length);
return rmPrefix.substring(0, rmPrefix.length - `~${hls}${exeExt}`.length);
});
myMap.set(hls, ghcs);
});
return myMap;
} else {
return null;
}
}
async function installationStatusOfGhcupTool(ghcup: GHCup, tool: Tool, version: string): Promise<ToolStatus> {
const b = await ghcup
.call(['whereis', tool, version], undefined, false)
.then(() => true)
.catch(() => false);
return new ToolStatus(tool, version, b);
}
/**
* Tracks the name, version and installation state of tools we need.
*/
class ToolStatus {
/**
* "\<name\>-\<version\>" of the installed Tool.
*/
readonly nameWithVersion: string = '';
/**
* Initialize an installed tool entry.
*
* If optional parameters are omitted, we assume the tool is installed.
*
* @param name Name of the tool.
* @param version Version of the tool, expected to be either SemVer or PVP versioned.
* @param installed Is this tool currently installed?
*/
public constructor(
readonly name: string,
readonly version: string,
readonly installed: boolean = true,
) {
this.nameWithVersion = `${name}-${version}`;
}
}
================================================
FILE: src/logger.ts
================================================
import { OutputChannel } from 'vscode';
import { Logger } from 'vscode-languageclient';
import * as fs from 'fs';
enum LogLevel {
Off,
Error,
Warn,
Info,
Debug,
}
export class ExtensionLogger implements Logger {
public readonly name: string;
public readonly level: LogLevel;
public readonly channel: OutputChannel;
public readonly logFile: string | undefined;
constructor(name: string, level: string, channel: OutputChannel, logFile: string | undefined) {
this.name = name;
this.level = this.getLogLevel(level);
this.channel = channel;
this.logFile = logFile;
}
public warn(message: string): void {
this.logLevel(LogLevel.Warn, message);
}
public info(message: string): void {
this.logLevel(LogLevel.Info, message);
}
public error(message: string) {
this.logLevel(LogLevel.Error, message);
}
public log(message: string) {
this.logLevel(LogLevel.Debug, message);
}
private write(msg: string) {
let now = new Date();
// Ugly hack to make js date iso format similar to hls one
const offset = now.getTimezoneOffset();
now = new Date(now.getTime() - offset * 60 * 1000);
const timedMsg = `${new Date().toISOString().replace('T', ' ').replace('Z', '0000')} ${msg}`;
this.channel.appendLine(timedMsg);
if (this.logFile) {
fs.appendFileSync(this.logFile, timedMsg + '\n');
}
}
private logLevel(level: LogLevel, msg: string) {
if (level <= this.level) {
this.write(`[${this.name}] ${LogLevel[level].toUpperCase()} ${msg}`);
}
}
private getLogLevel(level: string) {
switch (level) {
case 'off':
return LogLevel.Off;
case 'error':
return LogLevel.Error;
case 'debug':
return LogLevel.Debug;
default:
return LogLevel.Info;
}
}
}
================================================
FILE: src/metadata.ts
================================================
import * as fs from 'fs';
import * as https from 'https';
import * as path from 'path';
import { match } from 'ts-pattern';
import { promisify } from 'util';
import { window, workspace } from 'vscode';
import { Logger } from 'vscode-languageclient';
import { httpsGetSilently } from './utils';
/**
* Metadata of release information.
*
* Example of the expected format:
*
* ```
* {
* "1.6.1.0": {
* "A_64": {
* "Darwin": [
* "8.10.6",
* ],
* "Linux_Alpine": [
* "8.10.7",
* "8.8.4",
* ],
* },
* "A_ARM": {
* "Linux_UnknownLinux": [
* "8.10.7"
* ]
* },
* "A_ARM64": {
* "Darwin": [
* "8.10.7"
* ],
* "Linux_UnknownLinux": [
* "8.10.7"
* ]
* }
* }
* }
* ```
*
* consult [ghcup metadata repo](https://github.com/haskell/ghcup-metadata/) for details.
*/
export type ReleaseMetadata = Map<string, Map<string, Map<string, string[]>>>;
export type Platform = 'Darwin' | 'Linux_UnknownLinux' | 'Windows' | 'FreeBSD';
export type Arch = 'A_ARM' | 'A_ARM64' | 'A_32' | 'A_64';
/**
* Compute Map of supported HLS versions for this platform.
* Fetches HLS metadata information.
*
* @param storagePath Path to put in binary files and caches.
* @param logger Logger for feedback
* @returns Map of supported HLS versions or null if metadata could not be fetched.
*/
export async function getHlsMetadata(storagePath: string, logger: Logger): Promise<Map<string, string[]> | null> {
const metadata = await getReleaseMetadata(storagePath, logger).catch(() => null);
if (!metadata) {
window.showErrorMessage('Could not get release metadata');
return null;
}
const plat: Platform | null = match(process.platform)
.with('darwin', () => 'Darwin' as Platform)
.with('linux', () => 'Linux_UnknownLinux' as Platform)
.with('win32', () => 'Windows' as Platform)
.with('freebsd', () => 'FreeBSD' as Platform)
.otherwise(() => null);
if (plat === null) {
throw new Error(`Unknown platform ${process.platform}`);
}
const arch: Arch | null = match(process.arch)
.with('arm', () => 'A_ARM' as Arch)
.with('arm64', () => 'A_ARM64' as Arch)
.with('ia32', () => 'A_32' as Arch)
.with('x64', () => 'A_64' as Arch)
.otherwise(() => null);
if (arch === null) {
throw new Error(`Unknown architecture ${process.arch}`);
}
return findSupportedHlsPerGhc(plat, arch, metadata, logger);
}
/**
* Find all supported GHC versions per HLS version supported on the given
* platform and architecture.
* @param platform Platform of the host.
* @param arch Arch of the host.
* @param metadata HLS Metadata information.
* @param logger Logger.
* @returns Map from HLS version to GHC versions that are supported.
*/
export function findSupportedHlsPerGhc(
platform: Platform,
arch: Arch,
metadata: ReleaseMetadata,
logger: Logger,
): Map<string, string[]> {
logger.info(`Platform constants: ${platform}, ${arch}`);
const newMap = new Map<string, string[]>();
metadata.forEach((supportedArch, hlsVersion) => {
const supportedOs = supportedArch.get(arch);
if (supportedOs) {
const ghcSupportedOnOs = supportedOs.get(platform);
if (ghcSupportedOnOs) {
logger.log(`HLS ${hlsVersion} compatible with GHC Versions: ${ghcSupportedOnOs.join(',')}`);
// copy supported ghc versions to avoid unintended modifications
newMap.set(hlsVersion, [...ghcSupportedOnOs]);
}
}
});
return newMap;
}
/**
* Download GHCUP metadata.
*
* @param storagePath Path to put in binary files and caches.
* @param logger Logger for feedback.
* @returns Metadata of releases, or null if the cache can not be found.
*/
async function getReleaseMetadata(storagePath: string, logger: Logger): Promise<ReleaseMetadata | null> {
const releasesUrl = workspace.getConfiguration('haskell').releasesURL
? new URL(workspace.getConfiguration('haskell').releasesURL as string)
: undefined;
const opts: https.RequestOptions = releasesUrl
? {
host: releasesUrl.host,
path: releasesUrl.pathname,
}
: {
host: 'raw.githubusercontent.com',
path: '/haskell/ghcup-metadata/master/hls-metadata-0.0.1.json',
};
const offlineCache = path.join(storagePath, 'ghcupReleases.cache.json');
/**
* Convert a json value to ReleaseMetadata.
* Assumes the json is well-formed and a valid Release-Metadata.
* @param someObj Release Metadata without any typing information but well-formed.
* @returns Typed ReleaseMetadata.
*/
const objectToMetadata = (someObj: any): ReleaseMetadata => {
const obj = someObj as [string: [string: [string: string[]]]];
const hlsMetaEntries = Object.entries(obj).map(([hlsVersion, archMap]) => {
const archMetaEntries = Object.entries(archMap).map(([arch, supportedGhcVersionsPerOs]) => {
return [arch, new Map(Object.entries(supportedGhcVersionsPerOs))] as [string, Map<string, string[]>];
});
return [hlsVersion, new Map(archMetaEntries)] as [string, Map<string, Map<string, string[]>>];
});
return new Map(hlsMetaEntries);
};
async function readCachedReleaseData(): Promise<ReleaseMetadata | null> {
try {
logger.info(`Reading cached release data at ${offlineCache}`);
const cachedInfo = await promisify(fs.readFile)(offlineCache, { encoding: 'utf-8' });
// export type ReleaseMetadata = Map<string, Map<string, Map<string, string[]>>>;
const value: any = JSON.parse(cachedInfo);
return objectToMetadata(value);
} catch (err: any) {
// If file doesn't exist, return null, otherwise consider it a failure
if (err.code === 'ENOENT') {
logger.warn(`No cached release data found at ${offlineCache}`);
return null;
}
throw err;
}
}
try {
const releaseInfo = await httpsGetSilently(opts);
const releaseInfoParsed = JSON.parse(releaseInfo);
// Cache the latest successfully fetched release information
await promisify(fs.writeFile)(offlineCache, JSON.stringify(releaseInfoParsed), { encoding: 'utf-8' });
return objectToMetadata(releaseInfoParsed);
} catch (githubError: any) {
// Attempt to read from the latest cached file
try {
const cachedInfoParsed = await readCachedReleaseData();
window.showWarningMessage(
"Couldn't get the latest haskell-language-server releases from GitHub, used local cache instead: " +
githubError.message,
);
return cachedInfoParsed;
} catch (_fileError) {
throw new Error("Couldn't get the latest haskell-language-server releases from GitHub: " + githubError.message);
}
}
}
================================================
FILE: src/statusBar.ts
================================================
import * as vscode from 'vscode';
import * as constants from './commands/constants';
export class HaskellStatusBar {
readonly item: vscode.StatusBarItem;
constructor(readonly version?: string) {
// Set up the status bar item.
this.item = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);
}
refresh(): void {
const version = this.version ?? '<unknown>';
this.item.text = `Haskell`;
this.item.command = constants.OpenLogsCommandName;
this.item.tooltip = new vscode.MarkdownString('', true);
this.item.tooltip.isTrusted = true;
this.item.tooltip.appendMarkdown(
`[Extension Info](command:${constants.ShowExtensionVersions} "Show Extension Version"): Version ${version}\n\n` +
`---\n\n` +
`[$(terminal) Open Logs](command:${constants.OpenLogsCommandName} "Open the logs of the Server and Extension")\n\n` +
`[$(debug-restart) Restart Server](command:${constants.RestartServerCommandName} "Restart Haskell Language Server")\n\n` +
`[$(refresh) Restart Extension](command:${constants.RestartExtensionCommandName} "Restart vscode-haskell Extension")\n\n`,
);
}
show() {
this.item.show();
}
hide() {
this.item.hide();
}
dispose() {
this.item.dispose();
}
}
================================================
FILE: src/utils.ts
================================================
import * as child_process from 'child_process';
import * as fs from 'fs';
import * as https from 'https';
import * as os from 'os';
import * as process from 'process';
import { ProgressLocation, window, workspace, WorkspaceFolder } from 'vscode';
import { Logger } from 'vscode-languageclient';
import * as which from 'which';
import { HlsError } from './errors';
// Used for environment variables later on
export type IEnvVars = {
[key: string]: string;
};
/**
* Callback invoked on process termination.
*/
export type ProcessCallback = (
error: child_process.ExecFileException | null,
stdout: string,
stderr: string,
resolve: (value: string | PromiseLike<string>) => void,
reject: (reason?: HlsError | Error | string) => void,
) => void;
/**
* Call a process asynchronously.
* While doing so, update the windows with progress information.
* If you need to run a process, consider preferring this over running
* the command directly.
*
* @param binary Name of the binary to invoke.
* @param args Arguments passed directly to the binary.
* @param dir Directory in which the process shall be executed.
* @param logger Logger for progress updates.
* @param title Title of the action, shown to users if available.
* @param cancellable Can the user cancel this process invocation?
* @param envAdd Extra environment variables for this process only.
* @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.
* @returns Stdout of the process invocation, trimmed off newlines, or whatever the `callback` resolved to.
*/
export function callAsync(
binary: string,
args: string[],
logger: Logger,
dir?: string,
title?: string,
cancellable?: boolean,
envAdd?: IEnvVars,
callback?: ProcessCallback,
): Thenable<string> {
let newEnv: IEnvVars = resolveServerEnvironmentPATH(
workspace.getConfiguration('haskell').get('serverEnvironment') || {},
);
newEnv = { ...(process.env as IEnvVars), ...newEnv, ...(envAdd || {}) };
return window.withProgress(
{
location: ProgressLocation.Notification,
title,
cancellable,
},
async (_, token) => {
return new Promise<string>((resolve, reject) => {
const command: string = binary + ' ' + args.join(' ');
logger.info(`Executing '${command}' in cwd '${dir ? dir : process.cwd()}'`);
token.onCancellationRequested(() => {
logger.warn(`User canceled the execution of '${command}'`);
});
// Need to set the encoding to 'utf8' in order to get back a string
// We execute the command in a shell for windows, to allow use .cmd or .bat scripts
const childProcess = child_process
.execFile(
process.platform === 'win32' ? `"${binary}"` : binary,
args,
{ encoding: 'utf8', cwd: dir, shell: process.platform === 'win32', env: newEnv },
(err, stdout, stderr) => {
if (err) {
logger.error(`Error executing '${command}' with error code ${err.code}`);
logger.error(`stderr: ${stderr}`);
if (stdout) {
logger.error(`stdout: ${stdout}`);
}
}
if (callback) {
callback(err, stdout, stderr, resolve, reject);
} else {
if (err) {
reject(
Error(`\`${command}\` exited with exit code ${err.code}.
Consult the [Extensions Output](https://github.com/haskell/vscode-haskell#investigating-and-reporting-problems)
for details.`),
);
} else {
resolve(stdout?.trim());
}
}
},
)
.on('exit', (code, signal) => {
const msg =
`Execution of '${command}' terminated with code ${code}` + (signal ? `and signal ${signal}` : '');
logger.log(msg);
})
.on('error', (err) => {
if (err) {
logger.error(`Error executing '${command}': name = ${err.name}, message = ${err.message}`);
reject(err);
}
});
token.onCancellationRequested(() => childProcess.kill());
});
},
);
}
/**
* Compare the PVP versions of two strings.
* Details: https://github.com/haskell/pvp/
*
* @param l First version
* @param r second version
* @returns `1` if l is newer than r, `0` if they are equal and `-1` otherwise.
*/
export function comparePVP(l: string, r: string): number {
const al = l.split('.');
const ar = r.split('.');
let eq = 0;
for (let i = 0; i < Math.max(al.length, ar.length); i++) {
const el = parseInt(al[i], 10) || undefined;
const er = parseInt(ar[i], 10) || undefined;
if (el === undefined && er === undefined) {
break;
} else if (el !== undefined && er === undefined) {
eq = 1;
break;
} else if (el === undefined && er !== undefined) {
eq = -1;
break;
} else if (el !== undefined && er !== undefined && el > er) {
eq = 1;
break;
} else if (el !== undefined && er !== undefined && el < er) {
eq = -1;
break;
}
}
return eq;
}
/** When making http requests to github.com, use this header otherwise
* the server will close the request
*/
const userAgentHeader = { 'User-Agent': 'vscode-haskell' };
export async function httpsGetSilently(options: https.RequestOptions): Promise<string> {
const opts: https.RequestOptions = {
...options,
headers: {
...(options.headers ?? {}),
...userAgentHeader,
},
};
return new Promise((resolve, reject) => {
let data = '';
https
.get(opts, (res) => {
if (res.statusCode === 301 || res.statusCode === 302) {
if (!res.headers.location) {
reject(new Error('301/302 without a location header'));
return;
}
https.get(res.headers.location, (resAfterRedirect) => {
resAfterRedirect.on('data', (d) => (data += d));
resAfterRedirect.on('error', reject);
resAfterRedirect.on('close', () => {
resolve(data);
});
});
} else if (!res.statusCode || res.statusCode >= 400) {
reject(new Error(`Unexpected status code: ${res.statusCode}`));
} else {
res.on('data', (d) => (data += d));
res.on('error', reject);
res.on('close', () => {
resolve(data);
});
}
})
.on('error', reject);
});
}
/**
* Checks if the executable is on the PATH
* @param exe Name of the executable to find. Caller must ensure '.exe' extension is included on windows.
*/
export function executableExists(exe: string): boolean {
const isWindows = process.platform === 'win32';
let newEnv: IEnvVars = resolveServerEnvironmentPATH(
workspace.getConfiguration('haskell').get('serverEnvironment') || {},
);
newEnv = { ...(process.env as IEnvVars), ...newEnv };
const cmd: string = isWindows ? 'where' : 'which';
const out = child_process.spawnSync(cmd, [exe], { env: newEnv });
return out.status === 0 || (which.sync(exe, { nothrow: true, path: newEnv.PATH }) ?? '') !== '';
}
export function directoryExists(path: string): boolean {
return fs.existsSync(path) && fs.lstatSync(path).isDirectory();
}
export function expandHomeDir(path: string): string {
if (path.startsWith('~')) {
return path.replace('~', os.homedir);
}
return path;
}
export function resolvePathPlaceHolders(path: string, folder?: WorkspaceFolder) {
path = path.replace('${HOME}', os.homedir).replace('${home}', os.homedir).replace(/^~/, os.homedir);
if (folder) {
path = path.replace('${workspaceFolder}', folder.uri.path).replace('${workspaceRoot}', folder.uri.path);
}
return path;
}
export function resolvePATHPlaceHolders(path: string) {
return path
.replace('${HOME}', os.homedir)
.replace('${home}', os.homedir)
.replace('$PATH', process.env.PATH ?? '$PATH')
.replace('${PATH}', process.env.PATH ?? '${PATH}');
}
// also honours serverEnvironment.PATH
export function addPathToProcessPath(extraPath: string): string {
const pathSep = process.platform === 'win32' ? ';' : ':';
const serverEnvironment: IEnvVars = workspace.getConfiguration('haskell').get('serverEnvironment') || {};
const path: string[] = serverEnvironment.PATH
? serverEnvironment.PATH.split(pathSep).map((p) => resolvePATHPlaceHolders(p))
: (process.env.PATH?.split(pathSep) ?? []);
path.unshift(extraPath);
return path.join(pathSep);
}
export function resolveServerEnvironmentPATH(serverEnv: IEnvVars): IEnvVars {
const pathSep = process.platform === 'win32' ? ';' : ':';
const path: string[] | null = serverEnv.PATH
? serverEnv.PATH.split(pathSep).map((p) => resolvePATHPlaceHolders(p))
: null;
return {
...serverEnv,
...(path ? { PATH: path.join(pathSep) } : {}),
};
}
================================================
FILE: test/suite/extension.test.ts
================================================
// We have the following testing targets:
// 1. Test if the extension is present
// 2. Test if the extension can be activated
// 3. Test if the extension can create the extension log file
// 4. Test if the extension log contains server output (currently we use this to ensure the server is activated successfully)
// 5. Test if the server inherit environment variables defined in the settings
import * as vscode from 'vscode';
import * as assert from 'assert';
import * as path from 'path';
import * as fs from 'fs';
import { StopServerCommandName } from '../../src/commands/constants';
const LOG = 'hls.log';
const CACHE = 'cache-test';
const BIN = 'bin';
type AllowedKeys = typeof LOG | typeof CACHE;
suite('Extension Test Suite', () => {
const extension: vscode.Extension<unknown> | undefined = vscode.extensions.getExtension('haskell.haskell');
const haskellConfig = vscode.workspace.getConfiguration('haskell');
suiteSetup(async () => {
await haskellConfig.update('promptBeforeDownloads', false, vscode.ConfigurationTarget.Global);
await haskellConfig.update('manageHLS', 'GHCup');
await haskellConfig.update('logFile', LOG);
await haskellConfig.update('trace.server', 'messages');
await haskellConfig.update('releasesDownloadStoragePath', path.normalize(getWorkspaceFile(BIN).fsPath));
await haskellConfig.update('serverEnvironment', {
XDG_CACHE_HOME: path.normalize(getWorkspaceFile(CACHE).fsPath),
});
const contents = new TextEncoder().encode('main = putStrLn "hi vscode tests"');
await vscode.workspace.fs.writeFile(getWorkspaceFile('Main.hs'), contents);
});
test('1. Extension should be present', () => {
assert.ok(extension);
});
test('2. Extension can be activated', async () => {
assert.ok(await extension?.activate().then(() => true));
});
test('3. Extension should create the extension log file', async () => {
// Open the document to trigger the extension
vscode.workspace.openTextDocument(getWorkspaceFile('Main.hs'));
assert.ok(await runWithIntervalAndTimeout(() => workspaceFileExist(LOG), 1, 60));
});
test('4. Extension log should have server output', async () => {
vscode.workspace.openTextDocument(getWorkspaceFile('Main.hs'));
const checkServerLog = () => {
const logContents = getExtensionLogContent();
if (logContents) {
return logContents.match(/Registering IDE configuration/i) !== null;
}
return false;
};
assert.ok(await runWithIntervalAndTimeout(checkServerLog, 1, 60), 'Extension log file has no expected hls output');
});
test('5. Server should inherit environment variables defined in the settings', async () => {
vscode.workspace.openTextDocument(getWorkspaceFile('Main.hs'));
assert.ok(
await runWithIntervalAndTimeout(() => workspaceFileExist(CACHE), 1, 30),
'Server did not inherit XDG_CACHE_DIR from environment variables set in the settings',
);
});
suiteTeardown(async () => {
console.log('Stopping the lsp server');
await vscode.commands.executeCommand(StopServerCommandName);
console.log('Contents of the extension log:');
const logContents = getExtensionLogContent();
if (logContents) {
console.log(logContents);
}
});
});
//////////////////////////
// Helper functions BEGIN
//////////////////////////
function getWorkspaceRoot(): vscode.WorkspaceFolder {
const folders = vscode.workspace.workspaceFolders;
if (folders) {
return folders[0];
} else {
throw Error('workspaceFolders is empty');
}
}
function getWorkspaceFile(name: string): vscode.Uri {
const wsroot = getWorkspaceRoot().uri;
return wsroot.with({ path: path.posix.join(wsroot.path, name) });
}
/**
* Check if the given file exists in the workspace.
* @param key The key name
* @returns `True` if exists, otherwise `False`
*/
function workspaceFileExist(key: AllowedKeys): boolean {
const folder = getWorkspaceRoot();
const targetPath = path.join(folder.uri.fsPath, key);
return fs.existsSync(targetPath);
}
/**
* Run a function by given interval and timeout.
* @param fn The function to run, which has the signature `() => boolean`
* @param interval Interval in seconds
* @param timeout Interval in seconds
* @returns `true` if `fn` returns `true` before the `timeout`, otherwise `false`
*/
async function runWithIntervalAndTimeout(fn: () => boolean, interval: number, timeout: number): Promise<boolean> {
const startTime = Date.now();
const intervalMs = interval * 1000;
const timeoutMs = timeout * 1000;
const endTime = startTime + timeoutMs;
const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));
while (Date.now() <= endTime) {
if (fn()) {
return true;
}
await wait(intervalMs);
}
return false;
}
function getExtensionLogContent(): string | undefined {
const extLog = getWorkspaceFile(LOG).fsPath;
if (fs.existsSync(extLog)) {
const logContents = fs.readFileSync(extLog);
return logContents.toString();
} else {
console.log(`${extLog} does not exist!`);
return undefined;
}
}
//////////////////////////
// Helper functions END
//////////////////////////
================================================
FILE: test/suite/index.ts
================================================
import * as glob from 'glob';
import * as Mocha from 'mocha';
import * as path from 'path';
export async function run(): Promise<void> {
// Create the mocha test
const mocha = new Mocha({
ui: 'tdd',
timeout: 210_000, // 3.5 mins
color: true,
});
const testsRoot = path.resolve(__dirname, '..');
return new Promise((c, e) => {
glob
.glob('**/**.test.js', { cwd: testsRoot })
.then((files) => {
// Add files to the test suite
files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f)));
try {
// Run the mocha test
mocha.run((failures) => {
if (failures > 0) {
e(new Error(`${failures} tests failed.`));
} else {
c();
}
});
} catch (err) {
console.error(err);
e(err);
}
})
.catch((err) => e(err));
});
}
================================================
FILE: test-workspace/.gitkeep
================================================
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"module": "CommonJS",
"target": "ES2024",
"outDir": "out",
"lib": ["ES2024"],
"sourceMap": true,
"rootDir": ".",
"noUnusedLocals": true,
"strict": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"strictNullChecks": true,
"strictBuiltinIteratorReturn": false
},
"include": ["./src/**/*.ts", "./test/**/*.ts", "webpack.config.js"],
"exclude": ["node_modules", ".vscode", ".vscode-test"]
}
================================================
FILE: webpack.config.js
================================================
/*---------------------------------------------------------------------------------------------
* Minimal webpack config for VS Code extensions
* Uses ES Modules (compatible with Yarn 4, npm 11, and modern Node.js)
*--------------------------------------------------------------------------------------------*/
import path from 'path';
import { fileURLToPath } from 'url';
import ESLintPlugin from 'eslint-webpack-plugin';
// Recreate __dirname for ES Modules (not available in ESM by default)
const __dirname = path.dirname(fileURLToPath(import.meta.url));
/** @type {import('webpack').Configuration} */
export default {
// VS Code extensions run in a Node.js environment, not a browser
target: 'node',
// 'none' mode disables default optimizations (VS Code handles this)
mode: 'none',
// Entry point: where webpack starts bundling your extension
entry: './src/extension.ts',
output: {
// Output directory for the bundled extension
path: path.resolve(__dirname, 'dist'),
// Final bundle filename (must match 'main' in package.json)
filename: 'extension.js',
// Required format for VS Cod
gitextract_23g9bkdj/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ ├── dependabot.yml │ └── workflows/ │ ├── package.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode/ │ ├── extensions.json │ ├── launch.json │ ├── settings.json │ └── tasks.json ├── .vscode-test.js ├── .vscodeignore ├── Changelog.md ├── GenChangelogs.hs ├── LICENSE ├── README.md ├── docs/ │ ├── Contributing.md │ └── Release.md ├── eslint.config.mjs ├── flake.nix ├── 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 ├── test-workspace/ │ └── .gitkeep ├── tsconfig.json └── webpack.config.js
SYMBOL INDEX (109 symbols across 12 files)
FILE: src/config.ts
type LogLevel (line 8) | type LogLevel = 'off' | 'messages' | 'verbose';
type ClientLogLevel (line 9) | type ClientLogLevel = 'off' | 'error' | 'info' | 'debug';
type Config (line 11) | type Config = {
function initConfig (line 26) | function initConfig(workspaceConfig: WorkspaceConfiguration, uri: Uri, f...
function initLoggerFromConfig (line 57) | function initLoggerFromConfig(config: Config): ExtensionLogger {
function logConfig (line 61) | function logConfig(logger: Logger, config: Config) {
function getLogFile (line 76) | function getLogFile(workspaceConfig: WorkspaceConfiguration) {
function getClientLogLevel (line 85) | function getClientLogLevel(workspaceConfig: WorkspaceConfiguration): Cli...
function getLogLevel (line 105) | function getLogLevel(workspaceConfig: WorkspaceConfiguration): LogLevel {
function resolveLogFilePath (line 124) | function resolveLogFilePath(logFile: string | undefined, currentWorkingD...
function getServerArgs (line 128) | function getServerArgs(workspaceConfig: WorkspaceConfiguration, logLevel...
FILE: src/docsBrowser.ts
function showDocumentation (line 23) | async function showDocumentation({
function registerDocsBrowser (line 74) | function registerDocsBrowser(): Disposable {
function openDocumentationOnHackage (line 78) | async function openDocumentationOnHackage({
function registerDocsOpenOnHackage (line 98) | function registerDocsOpenOnHackage(): Disposable {
function hoverLinksMiddlewareHook (line 102) | function hoverLinksMiddlewareHook(
function completionLinksMiddlewareHook (line 117) | function completionLinksMiddlewareHook(
function processLink (line 142) | function processLink(ms: MarkdownString | MarkedString): string | Markdo...
FILE: src/errors.ts
class HlsError (line 3) | class HlsError extends Error {}
class MissingToolError (line 5) | class MissingToolError extends HlsError {
method constructor (line 7) | constructor(tool: string) {
method installLink (line 34) | public installLink(): Uri | null {
class NoMatchingHls (line 49) | class NoMatchingHls extends Error {
method constructor (line 50) | constructor(readonly ghcProjVersion: string) {
method docLink (line 53) | public docLink(): Uri {
FILE: src/extension.ts
type Client (line 21) | type Client = {
function activate (line 32) | async function activate(context: ExtensionContext) {
function activateServer (line 132) | async function activateServer(context: ExtensionContext, document: TextD...
function activateServerForFolder (line 149) | async function activateServerForFolder(context: ExtensionContext, uri: U...
function handleInitializationError (line 293) | async function handleInitializationError(e: unknown, logger: Logger) {
function initServerEnvironment (line 324) | function initServerEnvironment(config: Config, hlsExecutable: HlsExecuta...
function deactivate (line 339) | async function deactivate() {
FILE: src/ghcup.ts
type Tool (line 10) | type Tool = 'hls' | 'ghc' | 'cabal' | 'stack';
type ToolConfig (line 12) | type ToolConfig = Map<Tool, string | null>;
function initDefaultGHCup (line 14) | function initDefaultGHCup(config: GHCupConfig, logger: Logger, folder?: ...
type GHCupConfig (line 22) | type GHCupConfig = {
type ToolInfo (line 28) | type ToolInfo = {
class GHCup (line 34) | class GHCup {
method constructor (line 35) | constructor(
method call (line 50) | public async call(
method upgrade (line 72) | public async upgrade(): Promise<void> {
method getSetVersion (line 85) | public async getSetVersion(tool: Tool): Promise<ToolInfo | null> {
method getAnyLatestVersion (line 102) | public async getAnyLatestVersion(tool: Tool): Promise<ToolInfo | null> {
method getLatestAvailableVersion (line 121) | public async getLatestAvailableVersion(tool: Tool, tag: string = 'late...
method listTool (line 138) | private async listTool(tool: Tool, category: string): Promise<ToolInfo...
method findLatestUserInstalledTool (line 154) | public async findLatestUserInstalledTool(tool: Tool): Promise<ToolInfo> {
function findGHCup (line 164) | function findGHCup(logger: Logger, exePath?: string, folder?: WorkspaceF...
FILE: src/hlsBinaries.ts
type Context (line 18) | type Context = {
type ManageHLS (line 35) | type ManageHLS = 'GHCup' | 'PATH';
function fetchConfig (line 38) | function fetchConfig() {
function findServerExecutable (line 49) | function findServerExecutable(logger: Logger, folder?: WorkspaceFolder):...
function findHlsInPath (line 68) | function findHlsInPath(logger: Logger): string {
type HlsExecutable (line 82) | type HlsExecutable = HlsOnPath | HlsViaVSCodeConfig | HlsViaGhcup;
type HlsOnPath (line 84) | type HlsOnPath = {
type HlsViaVSCodeConfig (line 89) | type HlsViaVSCodeConfig = {
type HlsViaGhcup (line 94) | type HlsViaGhcup = {
function findHaskellLanguageServer (line 118) | async function findHaskellLanguageServer(
function promptUserForManagingHls (line 349) | async function promptUserForManagingHls(context: ExtensionContext, manag...
function getLatestProjectHls (line 383) | async function getLatestProjectHls(
function getProjectGhcVersion (line 430) | async function getProjectGhcVersion(
function getStoragePath (line 487) | function getStoragePath(context: ExtensionContext): string {
function findAvailableHlsBinariesFromGHCup (line 510) | async function findAvailableHlsBinariesFromGHCup(ghcup: GHCup): Promise<...
function installationStatusOfGhcupTool (line 537) | async function installationStatusOfGhcupTool(ghcup: GHCup, tool: Tool, v...
class ToolStatus (line 548) | class ToolStatus {
method constructor (line 563) | public constructor(
FILE: src/logger.ts
type LogLevel (line 5) | enum LogLevel {
class ExtensionLogger (line 12) | class ExtensionLogger implements Logger {
method constructor (line 18) | constructor(name: string, level: string, channel: OutputChannel, logFi...
method warn (line 24) | public warn(message: string): void {
method info (line 28) | public info(message: string): void {
method error (line 32) | public error(message: string) {
method log (line 36) | public log(message: string) {
method write (line 40) | private write(msg: string) {
method logLevel (line 52) | private logLevel(level: LogLevel, msg: string) {
method getLogLevel (line 58) | private getLogLevel(level: string) {
FILE: src/metadata.ts
type ReleaseMetadata (line 46) | type ReleaseMetadata = Map<string, Map<string, Map<string, string[]>>>;
type Platform (line 48) | type Platform = 'Darwin' | 'Linux_UnknownLinux' | 'Windows' | 'FreeBSD';
type Arch (line 50) | type Arch = 'A_ARM' | 'A_ARM64' | 'A_32' | 'A_64';
function getHlsMetadata (line 60) | async function getHlsMetadata(storagePath: string, logger: Logger): Prom...
function findSupportedHlsPerGhc (line 96) | function findSupportedHlsPerGhc(
function getReleaseMetadata (line 126) | async function getReleaseMetadata(storagePath: string, logger: Logger): ...
FILE: src/statusBar.ts
class HaskellStatusBar (line 4) | class HaskellStatusBar {
method constructor (line 6) | constructor(readonly version?: string) {
method refresh (line 11) | refresh(): void {
method show (line 27) | show() {
method hide (line 31) | hide() {
method dispose (line 35) | dispose() {
FILE: src/utils.ts
type IEnvVars (line 12) | type IEnvVars = {
type ProcessCallback (line 19) | type ProcessCallback = (
function callAsync (line 43) | function callAsync(
function comparePVP (line 125) | function comparePVP(l: string, r: string): number {
function httpsGetSilently (line 159) | async function httpsGetSilently(options: https.RequestOptions): Promise<...
function executableExists (line 202) | function executableExists(exe: string): boolean {
function directoryExists (line 213) | function directoryExists(path: string): boolean {
function expandHomeDir (line 217) | function expandHomeDir(path: string): string {
function resolvePathPlaceHolders (line 224) | function resolvePathPlaceHolders(path: string, folder?: WorkspaceFolder) {
function resolvePATHPlaceHolders (line 232) | function resolvePATHPlaceHolders(path: string) {
function addPathToProcessPath (line 241) | function addPathToProcessPath(extraPath: string): string {
function resolveServerEnvironmentPATH (line 251) | function resolveServerEnvironmentPATH(serverEnv: IEnvVars): IEnvVars {
FILE: test/suite/extension.test.ts
constant LOG (line 14) | const LOG = 'hls.log';
constant CACHE (line 15) | const CACHE = 'cache-test';
constant BIN (line 16) | const BIN = 'bin';
type AllowedKeys (line 17) | type AllowedKeys = typeof LOG | typeof CACHE;
function getWorkspaceRoot (line 87) | function getWorkspaceRoot(): vscode.WorkspaceFolder {
function getWorkspaceFile (line 96) | function getWorkspaceFile(name: string): vscode.Uri {
function workspaceFileExist (line 106) | function workspaceFileExist(key: AllowedKeys): boolean {
function runWithIntervalAndTimeout (line 120) | async function runWithIntervalAndTimeout(fn: () => boolean, interval: nu...
function getExtensionLogContent (line 137) | function getExtensionLogContent(): string | undefined {
FILE: test/suite/index.ts
function run (line 5) | async function run(): Promise<void> {
Condensed preview — 40 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (207K chars).
[
{
"path": ".github/ISSUE_TEMPLATE/bug_report.md",
"chars": 1466,
"preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: 'status: needs triage'\nassignees: ''\n--"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.md",
"chars": 634,
"preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n---\n\n**Is your feat"
},
{
"path": ".github/dependabot.yml",
"chars": 519,
"preview": "version: 2\nupdates:\n # NOTE: Dependabot official configuration documentation:\n # https://docs.github.com/en/code-secur"
},
{
"path": ".github/workflows/package.yml",
"chars": 748,
"preview": "on:\n push:\n branches:\n - master\n pull_request:\n branches:\n - '**'\n\njobs:\n build:\n strategy:\n "
},
{
"path": ".github/workflows/release.yml",
"chars": 1879,
"preview": "on:\n release:\n types: [prereleased, released]\n\nname: Deploy Extension\njobs:\n publish-extension:\n runs-on: ubuntu"
},
{
"path": ".github/workflows/test.yml",
"chars": 3114,
"preview": "on:\n push:\n branches:\n - master\n pull_request:\n branches:\n - '**'\n\njobs:\n build:\n strategy:\n "
},
{
"path": ".gitignore",
"chars": 183,
"preview": "out\r\nnode_modules\r\n.vscode-test\r\n.DS_Store\r\ndist\r\n*.vsix\r\n.husky\r\n\r\n# Ignore everything in test-workspace\r\ntest-workspac"
},
{
"path": ".prettierignore",
"chars": 82,
"preview": "node_modules/\ntest-workspace/\n.vscode/\n.vscode-test/\nout/\ndist/\nwebpack.config.js\n"
},
{
"path": ".prettierrc",
"chars": 34,
"preview": "printWidth: 120\nsingleQuote: true\n"
},
{
"path": ".vscode/extensions.json",
"chars": 89,
"preview": "{\n \"recommendations\": [\n \"dbaeumer.vscode-eslint\",\n \"esbenp.prettier-vscode\"\n ]\n}"
},
{
"path": ".vscode/launch.json",
"chars": 908,
"preview": "// A launch configuration that compiles the extension and then opens it inside a new window\n{\n \"version\": \"0.2.0\",\n \"c"
},
{
"path": ".vscode/settings.json",
"chars": 676,
"preview": "// Place your settings in this file to overwrite default and user settings.\n{\n \"files.exclude\": {\n \"dist\": true, // "
},
{
"path": ".vscode/tasks.json",
"chars": 959,
"preview": "// See https://go.microsoft.com/fwlink/?LinkId=733558\n// for the documentation about the tasks.json format\n{\n \"version\""
},
{
"path": ".vscode-test.js",
"chars": 412,
"preview": "const { defineConfig } = require('@vscode/test-cli');\n\nmodule.exports = defineConfig([\n {\n label: 'integration-tests"
},
{
"path": ".vscodeignore",
"chars": 140,
"preview": ".vscode/**\n.vscode-test/**\ntypings/**\nout/test/**\ntest/**\nsrc/**\n**/*.map\n.gitignore\ntsconfig.json\nnode_modules\nout\nsrc\n"
},
{
"path": "Changelog.md",
"chars": 26470,
"preview": "# Changelog for vscode-haskell\n\n## 2.8.0\n\n- Migrate project to npm 11.9.0\n ([#1336](https://github.com/haskell/vscode-h"
},
{
"path": "GenChangelogs.hs",
"chars": 1775,
"preview": "#!/usr/bin/env cabal\n{- cabal:\nbuild-depends: base, bytestring, process, text, github, time >= 1.9\n-}\n\n{-# LANGUAGE Over"
},
{
"path": "LICENSE",
"chars": 1270,
"preview": "Based on https://github.com/Microsoft/vscode-languageserver-node-example\nwhich has the following license requirement :\n\n"
},
{
"path": "README.md",
"chars": 16522,
"preview": "# Haskell for Visual Studio Code\n\n[](h"
},
{
"path": "docs/Contributing.md",
"chars": 2556,
"preview": "# Contributing\n\n## Dependencies and Building\n\nRun `npm install` in the project root to install the development dependenc"
},
{
"path": "docs/Release.md",
"chars": 3035,
"preview": "# Release Checklist\n\nFollow this list for items that must be completed for release of the `vscode-haskell` extension.\n\n-"
},
{
"path": "eslint.config.mjs",
"chars": 1388,
"preview": "import globals from 'globals';\nimport eslint from '@eslint/js';\nimport { defineConfig } from 'eslint/config';\nimport tse"
},
{
"path": "flake.nix",
"chars": 1821,
"preview": "{\n description = \"VS Code Haskell extension development environment\";\n\n inputs = {\n nixpkgs.url = \"github:NixOS/nix"
},
{
"path": "package.json",
"chars": 49270,
"preview": "{\n \"name\": \"haskell\",\n \"displayName\": \"Haskell\",\n \"description\": \"Haskell language support powered by the Haskell Lan"
},
{
"path": "src/commands/constants.ts",
"chars": 426,
"preview": "export const RestartExtensionCommandName = 'haskell.commands.restartExtension';\nexport const RestartServerCommandName = "
},
{
"path": "src/config.ts",
"chars": 5037,
"preview": "import { OutputChannel, Uri, window, WorkspaceConfiguration, WorkspaceFolder } from 'vscode';\nimport { expandHomeDir, IE"
},
{
"path": "src/docsBrowser.ts",
"chars": 5656,
"preview": "import { dirname } from 'path';\nimport {\n CancellationToken,\n commands,\n CompletionContext,\n CompletionItem,\n Compl"
},
{
"path": "src/errors.ts",
"chars": 1425,
"preview": "import { Uri } from 'vscode';\n\nexport class HlsError extends Error {}\n\nexport class MissingToolError extends HlsError {\n"
},
{
"path": "src/extension.ts",
"chars": 12032,
"preview": "import { commands, env, ExtensionContext, TextDocument, Uri, window, workspace, WorkspaceFolder } from 'vscode';\nimport "
},
{
"path": "src/ghcup.ts",
"chars": 7530,
"preview": "import * as path from 'path';\nimport * as os from 'os';\nimport * as process from 'process';\nimport { WorkspaceFolder } f"
},
{
"path": "src/hlsBinaries.ts",
"chars": 20168,
"preview": "import * as fs from 'fs';\nimport * as path from 'path';\nimport { ConfigurationTarget, ExtensionContext, window, workspac"
},
{
"path": "src/logger.ts",
"chars": 1823,
"preview": "import { OutputChannel } from 'vscode';\nimport { Logger } from 'vscode-languageclient';\nimport * as fs from 'fs';\n\nenum "
},
{
"path": "src/metadata.ts",
"chars": 6769,
"preview": "import * as fs from 'fs';\nimport * as https from 'https';\nimport * as path from 'path';\nimport { match } from 'ts-patter"
},
{
"path": "src/statusBar.ts",
"chars": 1280,
"preview": "import * as vscode from 'vscode';\nimport * as constants from './commands/constants';\n\nexport class HaskellStatusBar {\n "
},
{
"path": "src/utils.ts",
"chars": 9154,
"preview": "import * as child_process from 'child_process';\nimport * as fs from 'fs';\nimport * as https from 'https';\nimport * as os"
},
{
"path": "test/suite/extension.test.ts",
"chars": 5193,
"preview": "// We have the following testing targets:\n// 1. Test if the extension is present\n// 2. Test if the extension can be acti"
},
{
"path": "test/suite/index.ts",
"chars": 916,
"preview": "import * as glob from 'glob';\nimport * as Mocha from 'mocha';\nimport * as path from 'path';\n\nexport async function run()"
},
{
"path": "test-workspace/.gitkeep",
"chars": 0,
"preview": ""
},
{
"path": "tsconfig.json",
"chars": 514,
"preview": "{\n \"compilerOptions\": {\n \"module\": \"CommonJS\",\n \"target\": \"ES2024\",\n \"outDir\": \"out\",\n \"lib\": [\"ES2024\"],\n "
},
{
"path": "webpack.config.js",
"chars": 2431,
"preview": "/*---------------------------------------------------------------------------------------------\n * Minimal webpack conf"
}
]
About this extraction
This page contains the full source code of the alanz/vscode-hie-server GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 40 files (191.7 KB), approximately 48.6k tokens, and a symbol index with 109 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.